diff --git a/assets/controllers/organization_controller.js b/assets/controllers/organization_controller.js index 6271eb8..a999613 100644 --- a/assets/controllers/organization_controller.js +++ b/assets/controllers/organization_controller.js @@ -72,7 +72,7 @@ export default class extends Controller { formatterParams: { height: "50px", width: "50px", - urlPrefix: "", + urlPrefix: "/", urlSuffix: "", }, width: 100, diff --git a/assets/controllers/user_controller.js b/assets/controllers/user_controller.js index 44847d0..6ae7662 100644 --- a/assets/controllers/user_controller.js +++ b/assets/controllers/user_controller.js @@ -88,24 +88,27 @@ export default class extends Controller { headerSort: false, formatter: (cell) => { const data = cell.getRow().getData(); - const url = cell.getValue(); + let url = cell.getValue(); // use 'let' so we can modify it + + // 1. GENERATE INITIALS const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); const initials = `${first(data.name)}${first(data.prenom)}`; - // wrapper is for centering and circle clipping + // 2. CREATE WRAPPER const wrapper = document.createElement("div"); wrapper.className = "avatar-wrapper"; - // same size for both cases wrapper.style.width = "40px"; wrapper.style.height = "40px"; wrapper.style.display = "flex"; wrapper.style.alignItems = "center"; wrapper.style.justifyContent = "center"; wrapper.style.borderRadius = "50%"; - wrapper.style.overflow = "hidden"; // ensure image clips to circle + wrapper.style.overflow = "hidden"; - if (!url) { - wrapper.style.background = "#6c757d"; // gray background + // Helper to render fallback initials + const renderFallback = () => { + wrapper.innerHTML = ""; // clear any broken img + wrapper.style.background = "#6c757d"; const span = document.createElement("span"); span.className = "avatar-initials"; span.style.color = "#fff"; @@ -113,31 +116,36 @@ export default class extends Controller { span.style.fontSize = "14px"; span.textContent = initials || "•"; wrapper.appendChild(span); + }; + + // 3. IF NO URL, RENDER FALLBACK IMMEDIATELY + if (!url) { + renderFallback(); return wrapper; } - // Image case: make it fill the same wrapper + // --- THE FIX: HANDLE RELATIVE PATHS --- + // If the path doesn't start with 'http' or '/', add a leading '/' + // This ensures 'uploads/file.jpg' becomes '/uploads/file.jpg' + if (!url.startsWith('http') && !url.startsWith('/')) { + url = '/' + url; + } + + // 4. CREATE IMAGE const img = document.createElement("img"); img.src = url; img.alt = initials || "avatar"; img.style.width = "100%"; img.style.height = "100%"; - img.style.objectFit = "cover"; // keep aspect and cover circle + img.style.objectFit = "cover"; + + // 5. ERROR HANDLING (triggers your fallback) + img.onerror = () => { + console.warn("Image failed to load:", url); // Debug log to see the wrong path + renderFallback(); + }; + wrapper.appendChild(img); - - // Optional: fallback if image fails - img.addEventListener("error", () => { - wrapper.innerHTML = ""; - wrapper.style.background = "#6c757d"; - const span = document.createElement("span"); - span.className = "avatar-initials"; - span.style.color = "#fff"; - span.style.fontWeight = "600"; - span.style.fontSize = "12px"; - span.textContent = initials || "•"; - wrapper.appendChild(span); - }); - return wrapper; }, }, @@ -305,23 +313,27 @@ export default class extends Controller { headerSort: false, formatter: (cell) => { const data = cell.getRow().getData(); - const url = cell.getValue(); - const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); - const initials = `${data.initials}`; + let url = cell.getValue(); // use 'let' so we can modify it + // 1. GENERATE INITIALS + const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); + const initials = `${first(data.name)}${first(data.prenom)}`; + + // 2. CREATE WRAPPER const wrapper = document.createElement("div"); wrapper.className = "avatar-wrapper"; - // same size for both cases wrapper.style.width = "40px"; wrapper.style.height = "40px"; wrapper.style.display = "flex"; wrapper.style.alignItems = "center"; wrapper.style.justifyContent = "center"; wrapper.style.borderRadius = "50%"; - wrapper.style.overflow = "hidden"; // ensure image clips to circle + wrapper.style.overflow = "hidden"; - if (!url) { - wrapper.style.background = "#6c757d"; // gray background + // Helper to render fallback initials + const renderFallback = () => { + wrapper.innerHTML = ""; // clear any broken img + wrapper.style.background = "#6c757d"; const span = document.createElement("span"); span.className = "avatar-initials"; span.style.color = "#fff"; @@ -329,31 +341,36 @@ export default class extends Controller { span.style.fontSize = "14px"; span.textContent = initials || "•"; wrapper.appendChild(span); + }; + + // 3. IF NO URL, RENDER FALLBACK IMMEDIATELY + if (!url) { + renderFallback(); return wrapper; } - // Image case: make it fill the same wrapper + // --- THE FIX: HANDLE RELATIVE PATHS --- + // If the path doesn't start with 'http' or '/', add a leading '/' + // This ensures 'uploads/file.jpg' becomes '/uploads/file.jpg' + if (!url.startsWith('http') && !url.startsWith('/')) { + url = '/' + url; + } + + // 4. CREATE IMAGE const img = document.createElement("img"); img.src = url; img.alt = initials || "avatar"; img.style.width = "100%"; img.style.height = "100%"; - img.style.objectFit = "cover"; // keep aspect and cover circle + img.style.objectFit = "cover"; + + // 5. ERROR HANDLING (triggers your fallback) + img.onerror = () => { + console.warn("Image failed to load:", url); // Debug log to see the wrong path + renderFallback(); + }; + wrapper.appendChild(img); - - // Optional: fallback if image fails - img.addEventListener("error", () => { - wrapper.innerHTML = ""; - wrapper.style.background = "#6c757d"; - const span = document.createElement("span"); - span.className = "avatar-initials"; - span.style.color = "#fff"; - span.style.fontWeight = "600"; - span.style.fontSize = "12px"; - span.textContent = initials || "•"; - wrapper.appendChild(span); - }); - return wrapper; }, }, @@ -411,23 +428,27 @@ export default class extends Controller { headerSort: false, formatter: (cell) => { const data = cell.getRow().getData(); - const url = cell.getValue(); - const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); - const initials = `${data.initials}`; + let url = cell.getValue(); // use 'let' so we can modify it + // 1. GENERATE INITIALS + const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); + const initials = `${first(data.name)}${first(data.prenom)}`; + + // 2. CREATE WRAPPER const wrapper = document.createElement("div"); wrapper.className = "avatar-wrapper"; - // same size for both cases wrapper.style.width = "40px"; wrapper.style.height = "40px"; wrapper.style.display = "flex"; wrapper.style.alignItems = "center"; wrapper.style.justifyContent = "center"; wrapper.style.borderRadius = "50%"; - wrapper.style.overflow = "hidden"; // ensure image clips to circle + wrapper.style.overflow = "hidden"; - if (!url) { - wrapper.style.background = "#6c757d"; // gray background + // Helper to render fallback initials + const renderFallback = () => { + wrapper.innerHTML = ""; // clear any broken img + wrapper.style.background = "#6c757d"; const span = document.createElement("span"); span.className = "avatar-initials"; span.style.color = "#fff"; @@ -435,31 +456,36 @@ export default class extends Controller { span.style.fontSize = "14px"; span.textContent = initials || "•"; wrapper.appendChild(span); + }; + + // 3. IF NO URL, RENDER FALLBACK IMMEDIATELY + if (!url) { + renderFallback(); return wrapper; } - // Image case: make it fill the same wrapper + // --- THE FIX: HANDLE RELATIVE PATHS --- + // If the path doesn't start with 'http' or '/', add a leading '/' + // This ensures 'uploads/file.jpg' becomes '/uploads/file.jpg' + if (!url.startsWith('http') && !url.startsWith('/')) { + url = '/' + url; + } + + // 4. CREATE IMAGE const img = document.createElement("img"); img.src = url; img.alt = initials || "avatar"; img.style.width = "100%"; img.style.height = "100%"; - img.style.objectFit = "cover"; // keep aspect and cover circle + img.style.objectFit = "cover"; + + // 5. ERROR HANDLING (triggers your fallback) + img.onerror = () => { + console.warn("Image failed to load:", url); // Debug log to see the wrong path + renderFallback(); + }; + wrapper.appendChild(img); - - // Optional: fallback if image fails - img.addEventListener("error", () => { - wrapper.innerHTML = ""; - wrapper.style.background = "#6c757d"; - const span = document.createElement("span"); - span.className = "avatar-initials"; - span.style.color = "#fff"; - span.style.fontWeight = "600"; - span.style.fontSize = "12px"; - span.textContent = initials || "•"; - wrapper.appendChild(span); - }); - return wrapper; }, }, @@ -539,23 +565,27 @@ export default class extends Controller { headerSort: false, formatter: (cell) => { const data = cell.getRow().getData(); - const url = cell.getValue(); + let url = cell.getValue(); // use 'let' so we can modify it + + // 1. GENERATE INITIALS const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); const initials = `${first(data.name)}${first(data.prenom)}`; + // 2. CREATE WRAPPER const wrapper = document.createElement("div"); wrapper.className = "avatar-wrapper"; - // same size for both cases wrapper.style.width = "40px"; wrapper.style.height = "40px"; wrapper.style.display = "flex"; wrapper.style.alignItems = "center"; wrapper.style.justifyContent = "center"; wrapper.style.borderRadius = "50%"; - wrapper.style.overflow = "hidden"; // ensure image clips to circle + wrapper.style.overflow = "hidden"; - if (!url) { - wrapper.style.background = "#6c757d"; // gray background + // Helper to render fallback initials + const renderFallback = () => { + wrapper.innerHTML = ""; // clear any broken img + wrapper.style.background = "#6c757d"; const span = document.createElement("span"); span.className = "avatar-initials"; span.style.color = "#fff"; @@ -563,31 +593,36 @@ export default class extends Controller { span.style.fontSize = "14px"; span.textContent = initials || "•"; wrapper.appendChild(span); + }; + + // 3. IF NO URL, RENDER FALLBACK IMMEDIATELY + if (!url) { + renderFallback(); return wrapper; } - // Image case: make it fill the same wrapper + // --- THE FIX: HANDLE RELATIVE PATHS --- + // If the path doesn't start with 'http' or '/', add a leading '/' + // This ensures 'uploads/file.jpg' becomes '/uploads/file.jpg' + if (!url.startsWith('http') && !url.startsWith('/')) { + url = '/' + url; + } + + // 4. CREATE IMAGE const img = document.createElement("img"); img.src = url; img.alt = initials || "avatar"; img.style.width = "100%"; img.style.height = "100%"; - img.style.objectFit = "cover"; // keep aspect and cover circle + img.style.objectFit = "cover"; + + // 5. ERROR HANDLING (triggers your fallback) + img.onerror = () => { + console.warn("Image failed to load:", url); // Debug log to see the wrong path + renderFallback(); + }; + wrapper.appendChild(img); - - // Optional: fallback if image fails - img.addEventListener("error", () => { - wrapper.innerHTML = ""; - wrapper.style.background = "#6c757d"; - const span = document.createElement("span"); - span.className = "avatar-initials"; - span.style.color = "#fff"; - span.style.fontWeight = "600"; - span.style.fontSize = "12px"; - span.textContent = initials || "•"; - wrapper.appendChild(span); - }); - return wrapper; }, }, diff --git a/config/services.yaml b/config/services.yaml index 0c757b9..a438986 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -6,6 +6,7 @@ parameters: aws_url: '%env(AWS_ENDPOINT)%' aws_public_url: '%env(AWS_ENDPOINT)%' + aws_bucket: '%env(S3_PORTAL_BUCKET)%' logos_directory: '%kernel.project_dir%/public/uploads/logos' services: diff --git a/src/Controller/OrganizationController.php b/src/Controller/OrganizationController.php index 9691197..08642a1 100644 --- a/src/Controller/OrganizationController.php +++ b/src/Controller/OrganizationController.php @@ -232,6 +232,7 @@ class OrganizationController extends AbstractController try { $organization->setIsActive(false); $organization->setIsDeleted(true); + $this->organizationsService->deleteLogo($organization); // Deactivate all associated UsersOrganizations $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization); @@ -344,12 +345,12 @@ class OrganizationController extends AbstractController // Map to array $data = array_map(function (Organizations $org) { - $picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $org->getLogoUrl(); + return [ 'id' => $org->getId(), 'name' => $org->getName(), 'email' => $org->getEmail(), - 'logoUrl' => $picture ?: null, + 'logoUrl' => $org->getLogoUrl() ?: null, 'active' => $org->isActive(), 'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]), ]; diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 1f4ef43..0a67b16 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -128,12 +128,15 @@ class UserController extends AbstractController 'isActive' => true, ]); if (!$uoList) { - $this->loggerService->logEntityNotFound('UsersOrganization', [ - 'user_id' => $user->getId(), - 'organization_id' => $orgId], - $actingUser->getId()); - $this->addFlash('error', "L'utilisateur n'est pas actif dans une organisation."); - throw $this->createNotFoundException(self::NOT_FOUND); + $data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser, true); + return $this->render('user/show.html.twig', [ + 'user' => $user, + 'organizationId' => $orgId ?? null, + 'uoActive' => $uoActive ?? null, + 'apps' => $apps ?? [], + 'data' => $data ?? [], + 'canEdit' => false, + ]); } } // Charger les liens UserOrganizationApp (UOA) actifs pour les UO trouvées @@ -168,7 +171,11 @@ class UserController extends AbstractController $canEdit = $this->userService->canEditRolesCheck($actingUser, $user,$this->isGranted('ROLE_ADMIN'), $singleUo, $organization); } catch (\Exception $e) { - $this->errorLogger->error($e->getMessage()); + $this->loggerService->logError('error while loading user information', [ + 'target_user_id' => $id, + 'acting_user_id' => $actingUser->getId(), + 'error' => $e->getMessage(), + ]); $this->addFlash('error', 'Une erreur est survenue lors du chargement des informations utilisateur.'); $referer = $request->headers->get('referer'); return $this->redirect($referer ?? $this->generateUrl('app_index')); @@ -183,7 +190,7 @@ class UserController extends AbstractController ]); } - #[Route('/edit/{id}', name: 'edit', methods: ['GET', 'POST'])] + #[Route('/edit/{id}', name: 'edit', methods: ['GET','POST'])] public function edit(int $id, Request $request): Response { $this->denyAccessUnlessGranted('ROLE_USER'); @@ -199,13 +206,12 @@ class UserController extends AbstractController $form = $this->createForm(UserForm::class, $user); $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { + // Handle user edit - $picture = $form->get('pictureUrl')->getData(); + $picture = $form->get('pictureUrl')->getData();; $this->userService->formatUserData($user, $picture); $user->setModifiedAt(new \DateTimeImmutable('now')); - $this->entityManager->persist($user); $this->entityManager->flush(); @@ -571,6 +577,7 @@ class UserController extends AbstractController $user->setIsActive(false); $user->setIsDeleted(true); + $this->userService->deleteProfilePicture($user); $user->setModifiedAt(new \DateTimeImmutable('now')); // Deactivate all org links $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user); @@ -731,10 +738,9 @@ class UserController extends AbstractController // Map to array $data = array_map(function (User $user) { - $picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl(); return [ 'id' => $user->getId(), - 'pictureUrl' => $picture, + 'pictureUrl' => $user->getPictureUrl(), 'name' => $user->getSurname(), 'prenom' => $user->getName(), 'email' => $user->getEmail(), @@ -786,10 +792,9 @@ class UserController extends AbstractController // Map to array (keep isConnected) $data = array_map(function (UsersOrganizations $uo) { $user = $uo->getUsers(); - $picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl(); $initials = $user->getName()[0] . $user->getSurname()[0]; return [ - 'pictureUrl' => $picture, + 'pictureUrl' =>$user->getPictureUrl(), 'email' => $user->getEmail(), 'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()), 'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]), @@ -829,10 +834,9 @@ class UserController extends AbstractController // Map to array (keep isConnected) $data = array_map(function (UsersOrganizations $uo) { $user = $uo->getUsers(); - $picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl(); $initials = $user->getName()[0] . $user->getSurname()[0]; return [ - 'pictureUrl' => $picture, + 'pictureUrl' => $user->getPictureUrl(), 'email' => $user->getEmail(), 'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()), 'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]), diff --git a/src/Service/OrganizationsService.php b/src/Service/OrganizationsService.php index 25e2400..c236ba3 100644 --- a/src/Service/OrganizationsService.php +++ b/src/Service/OrganizationsService.php @@ -30,25 +30,86 @@ class OrganizationsService public function handleLogo(Organizations $organization, $logoFile): void { + // 1. Define the destination directory (adjust path as needed, e.g., 'public/uploads/profile_pictures') + $destinationDir = 'uploads/organization_logos'; + + // 2. Create the directory if it doesn't exist + if (!file_exists($destinationDir)) { + // 0755 is the standard permission (Owner: read/write/exec, Others: read/exec) + if (!mkdir($destinationDir, 0755, true) && !is_dir($destinationDir)) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $destinationDir)); + } + } $extension = $logoFile->guessExtension(); - $customFilename = $organization->getName() . '_' . date('dmyHis') . "." . $extension; + // Sanitize the filename to remove special characters/spaces to prevent filesystem errors + $safeName = preg_replace('/[^a-zA-Z0-9]/', '', $organization->getName()); + $customFilename = $safeName . '_' . date('dmyHis') . '.' . $extension; try { - $this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $logoFile, $customFilename, $extension, 'logo/'); - $this->loggerService->logAWSAction('Upload organization logo', [ - 'organization_id' => $organization->getId(), - 'filename' => $customFilename, - 'bucket' => $_ENV['S3_PORTAL_BUCKET'], + // 4. Move the file to the destination directory + // The move() method is standard in Symfony/Laravel UploadedFile objects + $logoFile->move($destinationDir, $customFilename); + + // 5. Update the user entity with the relative path + // Ensure you store the path relative to your public folder usually + $organization->setLogoUrl($destinationDir . '/' . $customFilename); + + } catch (\Exception $e) { + // 6. Log the critical error as requested + $this->loggerService->logError('File upload failed',[ + 'target_organization_id' => $organization->getId(), + 'message' => $e->getMessage(), + 'file_name' => $customFilename, ]); - $organization->setLogoUrl('logo/' . $customFilename); - } catch (FileException $e) { - $this->loggerService->logError('Failed to upload organization logo to S3', [ - 'organization_id' => $organization->getId(), - 'error' => $e->getMessage(), - 'bucket' => $_ENV['S3_PORTAL_BUCKET'], + + // Optional: Re-throw the exception if you want the controller/user to know the upload failed + throw new FileException('File upload failed.'); + } + } + + public function deleteLogo(Organizations $organization): void + { + // 1. Get the current picture path from the user entity + $currentPicturePath = $organization->getLogoUrl(); + + // If the user doesn't have a picture, simply return (nothing to delete) + if (!$currentPicturePath) { + return; + } + + try { + // 2. Check if the file exists on the server before trying to delete + // Note: Ensure $currentPicturePath is relative to the script execution or absolute. + if (file_exists($currentPicturePath)) { + + // 3. Delete the file + if (!unlink($currentPicturePath)) { + throw new \Exception(sprintf('Could not unlink "%s"', $currentPicturePath)); + } + } else { + // Optional: Log a warning if the DB had a path but the file was missing + $this->loggerService->logError('File not found on disk during deletion request', [ + 'target_organization_id' => $organization->getId(), + 'missing_path' => $currentPicturePath + ]); + } + + // 4. Update the user entity to remove the reference + $organization->setLogoUrl(""); + + } catch (\Exception $e) { + // 5. Log the critical error + // We log it, but strictly speaking, we might still want to nullify the DB + // if the file is corrupted/un-deletable to prevent broken images on the frontend. + $this->loggerService->logError('File deletion failed', [ + 'target_organization_id' => $organization->getId(), + 'message' => $e->getMessage(), + 'file_path' => $currentPicturePath, ]); - throw new FileException('Failed to upload logo to S3: ' . $e->getMessage()); + + // Re-throw if you want to stop execution/show error to user + throw new \RuntimeException('Unable to remove profile picture.'); } } diff --git a/src/Service/UserService.php b/src/Service/UserService.php index 3e7ae08..c762440 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -19,6 +19,7 @@ use RuntimeException; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\File\Exception\FileException; use App\Event\UserCreatedEvent; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class UserService @@ -173,28 +174,91 @@ class UserService return ['none' => $group]; } - public function handleProfilePicture(User $user, $picture): void + public function handleProfilePicture(User $user, UploadedFile $picture): void { - // Get file extension - $extension = $picture->guessExtension(); + // 1. Define the destination directory (adjust path as needed, e.g., 'public/uploads/profile_pictures') + $destinationDir = 'uploads/profile_pictures'; - // Create custom filename: userNameUserSurname_dmyHis - $customFilename = $user->getName() . $user->getSurname() . '_' . date('dmyHis') . '.' . $extension; - try { - $this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $picture, $customFilename, $extension, 'profile/'); - $this->loggerService->logAWSAction( - 'Profile picture uploaded to S3', [ - 'user_id' => $user->getId(), - 'filename' => $customFilename, - ]); - $user->setPictureUrl('profile/' . $customFilename); - } catch (FileException $e) { - // Handle upload error - throw new FileException('File upload failed: ' . $e->getMessage()); + // 2. Create the directory if it doesn't exist + if (!file_exists($destinationDir)) { + // 0755 is the standard permission (Owner: read/write/exec, Others: read/exec) + if (!mkdir($destinationDir, 0755, true) && !is_dir($destinationDir)) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $destinationDir)); + } } -// -// + // 3. Get extension and create a safe filename + $extension = $picture->guessExtension(); + + // Sanitize the filename to remove special characters/spaces to prevent filesystem errors + $safeName = preg_replace('/[^a-zA-Z0-9]/', '', $user->getName() . $user->getSurname()); + $customFilename = $safeName . '_' . date('dmyHis') . '.' . $extension; + + try { + // 4. Move the file to the destination directory + // The move() method is standard in Symfony/Laravel UploadedFile objects + $picture->move($destinationDir, $customFilename); + + // 5. Update the user entity with the relative path + // Ensure you store the path relative to your public folder usually + $user->setPictureUrl($destinationDir . '/' . $customFilename); + + } catch (\Exception $e) { + // 6. Log the critical error as requested + $this->loggerService->logError('File upload failed',[ + 'target_user_id' => $user->getId(), + 'message' => $e->getMessage(), + 'file_name' => $customFilename, + ]); + + // Optional: Re-throw the exception if you want the controller/user to know the upload failed + throw new FileException('File upload failed.'); + } + } + + public function deleteProfilePicture(User $user): void + { + // 1. Get the current picture path from the user entity + $currentPicturePath = $user->getPictureUrl(); + + // If the user doesn't have a picture, simply return (nothing to delete) + if (!$currentPicturePath) { + return; + } + + try { + // 2. Check if the file exists on the server before trying to delete + // Note: Ensure $currentPicturePath is relative to the script execution or absolute. + if (file_exists($currentPicturePath)) { + + // 3. Delete the file + if (!unlink($currentPicturePath)) { + throw new \Exception(sprintf('Could not unlink "%s"', $currentPicturePath)); + } + } else { + // Optional: Log a warning if the DB had a path but the file was missing + $this->loggerService->logError('File not found on disk during deletion request', [ + 'target_user_id' => $user->getId(), + 'missing_path' => $currentPicturePath + ]); + } + + // 4. Update the user entity to remove the reference + $user->setPictureUrl(""); + + } catch (\Exception $e) { + // 5. Log the critical error + // We log it, but strictly speaking, we might still want to nullify the DB + // if the file is corrupted/un-deletable to prevent broken images on the frontend. + $this->loggerService->logError('File deletion failed', [ + 'target_user_id' => $user->getId(), + 'message' => $e->getMessage(), + 'file_path' => $currentPicturePath, + ]); + + // Re-throw if you want to stop execution/show error to user + throw new \RuntimeException('Unable to remove profile picture.'); + } } /** @@ -297,7 +361,7 @@ class UserService { $formatted = array_map(function (UsersOrganizations $uo) { $user = $uo->getUsers(); - $picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl(); + if ($uo->getStatut() === "INVITED") { $statut = "INVITED"; // if user invited but not accepted in 1 hour, set statut to EXPIRED @@ -311,7 +375,7 @@ class UserService $statut = $uo->isActive() ? "ACTIVE" : "INACTIVE"; } return [ - 'pictureUrl' => $picture, + 'pictureUrl' => $user->getPictureUrl(), 'name' => $user->getSurname(), 'prenom' => $user->getName(), 'email' => $user->getEmail(), diff --git a/templates/organization/show.html.twig b/templates/organization/show.html.twig index 95b045c..a24e1b2 100644 --- a/templates/organization/show.html.twig +++ b/templates/organization/show.html.twig @@ -12,7 +12,7 @@
{% if organization.logoUrl %} - Organization logo {% endif %}

{{ organization.name|title }} - Dashboard

diff --git a/templates/user/edit.html.twig b/templates/user/edit.html.twig index 6dee523..b89017f 100644 --- a/templates/user/edit.html.twig +++ b/templates/user/edit.html.twig @@ -14,7 +14,7 @@
- {{ form_start(form, {'action': path('user_edit', {'id': user.id}), 'method': 'PUT'}) }} + {{ form_start(form, {'action': path('user_edit', {'id': user.id}), 'method': 'POST'}) }} {{ form_widget(form) }} diff --git a/templates/user/userInformation.html.twig b/templates/user/userInformation.html.twig index 4d1366e..f74c11c 100644 --- a/templates/user/userInformation.html.twig +++ b/templates/user/userInformation.html.twig @@ -3,7 +3,7 @@
{% if user.pictureUrl is not empty %} - user {% endif %}