removed S3 and handles bugs that came with it

This commit is contained in:
Charles 2026-01-28 10:08:55 +01:00
parent 3d23e9cec3
commit 59bc78fe47
6 changed files with 232 additions and 128 deletions

View File

@ -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;
},
},

View File

@ -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:

View File

@ -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()]),

View File

@ -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(),

View File

@ -14,7 +14,7 @@
<div class="card-body">
{{ 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) }}
<input hidden type="text" value="{{ organizationId }}" name="organizationId">
<button type="submit" class="btn btn-primary">Enregistrer</button>

View File

@ -3,7 +3,7 @@
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex gap-2">
{% if user.pictureUrl is not empty %}
<img src="{{ aws_url ~ user.pictureUrl }}" alt="user" class="rounded-circle"
<img src="{{asset(user.pictureUrl)}}" alt="user" class="rounded-circle"
style="width:40px; height:40px;">
{% endif %}
<div class="card-title ">