diff --git a/assets/app.js b/assets/app.js index 2e86eec..a261fc3 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1,7 +1,6 @@ import './app.scss' import * as Turbo from "@hotwired/turbo" -import * as Turbo from "@hotwired/turbo"; // --- INITIALISATION SENTRY (En premier !) --- Sentry.init({ diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 9fe36dc..05d8a21 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -41,7 +41,9 @@ security: App\Entity\Account: 'auto' role_hierarchy: - ROLE_ROOT: [ROLE_ADMIN] + ROLE_ROOT: [ROLE_ADMIN,ROLE_CLIENT_MAIN] + ROLE_CLIENT_MAIN: [ROLE_ADMIN] + access_control: # Permettre l'accès aux pages 2FA même si on n'est pas encore pleinement "ROLE_ADMIN" diff --git a/migrations/Version20260115194430.php b/migrations/Version20260115194430.php new file mode 100644 index 0000000..643f8a0 --- /dev/null +++ b/migrations/Version20260115194430.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE audit_log ADD hash_code VARCHAR(64) NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE audit_log DROP hash_code'); + } +} diff --git a/src/Controller/Dashboard/AccountController.php b/src/Controller/Dashboard/AccountController.php index 76b38a5..e3bbd38 100644 --- a/src/Controller/Dashboard/AccountController.php +++ b/src/Controller/Dashboard/AccountController.php @@ -5,10 +5,15 @@ namespace App\Controller\Dashboard; use App\Entity\Account; use App\Event\Object\EventAdminCreate; use App\Event\Object\EventAdminDeleted; +use App\Form\AccountPasswordType; use App\Form\AccountType; use App\Logger\AppLogger; +use App\Repository\AccountLoginRegisterRepository; use App\Repository\AccountRepository; +use App\Repository\AuditLogRepository; +use App\Service\Mailer\Mailer; use Doctrine\ORM\EntityManagerInterface; +use Knp\Component\Pager\PaginatorInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; @@ -23,10 +28,10 @@ class AccountController extends AbstractController * Liste des administrateurs */ #[Route(path: '/crm/administrateur', name: 'app_crm_administrateur', options: ['sitemap' => false], methods: ['GET'])] - public function administrateur(AccountRepository $accountRepository, AppLogger $appLogger,EventDispatcherInterface $eventDispatcher): Response + public function administrateur(AccountRepository $accountRepository, AppLogger $appLogger): Response { - // Audit Log : On trace la consultation de la liste - $appLogger->record('VIEW', 'Consultation de la liste des administrateurs'); + $appLogger->record('VIEW', 'Consultation de la liste des Administrateurs'); + return $this->render('dashboard/administrateur.twig', [ 'admins' => $accountRepository->findAdmin(), ]); @@ -37,15 +42,14 @@ class AccountController extends AbstractController */ #[Route(path: '/crm/administrateur/add', name: 'app_crm_administrateur_add', options: ['sitemap' => false], methods: ['GET', 'POST'])] public function administrateurAdd( - Request $request, - EntityManagerInterface $entityManager, + Request $request, + EntityManagerInterface $entityManager, UserPasswordHasherInterface $passwordHasher, - AppLogger $appLogger, - EventDispatcherInterface $eventDispatcher - ): Response { + AppLogger $appLogger, + EventDispatcherInterface $eventDispatcher + ): Response + { $account = new Account(); - - // Initialisation des valeurs par défaut $account->setIsFirstLogin(true); $account->setIsActif(false); $account->setUuid(Uuid::v4()->toRfc4122()); @@ -55,108 +59,272 @@ class AccountController extends AbstractController $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - // Hachage du mot de passe temporaire $tempPassword = bin2hex(random_bytes(20)); - $hashedPassword = $passwordHasher->hashPassword($account, $tempPassword); - $account->setPassword($hashedPassword); + $account->setPassword($passwordHasher->hashPassword($account, $tempPassword)); $entityManager->persist($account); $entityManager->flush(); - // Audit Log : Enregistrement de la création $appLogger->record( 'CREATE', - sprintf("Création de l'administrateur : %s %s (%s)", - $account->getFirstName(), - $account->getName(), - $account->getEmail() + sprintf("Création de l'Administrateur : %s %s (%s)", + $account->getFirstName(), $account->getName(), $account->getEmail() ) ); - // Notification : Envoi du mail d'activation $eventDispatcher->dispatch(new EventAdminCreate($account, $this->getUser())); - - $this->addFlash('success', 'Le compte administrateur de ' . $account->getFirstName() . ' a été créé avec succès.'); + $this->addFlash('success', "L'Administrateur " . $account->getFirstName() . " a été créé avec succès."); return $this->redirectToRoute('app_crm_administrateur'); } - return $this->render('dashboard/administrateur/add.twig', [ - 'form' => $form->createView(), - ]); + return $this->render('dashboard/administrateur/add.twig', ['form' => $form->createView()]); } - /** - * Voir la fiche d'un administrateur - */ - #[Route(path: '/crm/administrateur/{id}', name: 'app_crm_administrateur_view', options: ['sitemap' => false], methods: ['GET'])] - public function administrateurView(?Account $account, AppLogger $appLogger): Response - { + #[Route(path: '/crm/administrateur/{id}', name: 'app_crm_administrateur_view', options: ['sitemap' => false], methods: ['GET', 'POST'])] + public function administrateurView( + ?Account $account, + PaginatorInterface $paginator, + Request $request, + AppLogger $appLogger, + Mailer $mailer, + AccountLoginRegisterRepository $accountLoginRegisterRepository, + EntityManagerInterface $em, + AuditLogRepository $auditLogRepository, + UserPasswordHasherInterface $passwordHasher + ): Response { if (!$account) { $this->addFlash('error', 'Administrateur introuvable.'); return $this->redirectToRoute('app_crm_administrateur'); } - // Audit Log : On trace la consultation d'une fiche spécifique - $appLogger->record( - 'VIEW', - sprintf("Consultation du profil de : %s %s (ID: %d)", - $account->getFirstName(), - $account->getName(), - $account->getId() - ) + // --- SÉCURITÉ : PROTECTION ROOT --- + if (in_array('ROLE_ROOT', $account->getRoles()) && !$this->isGranted('ROLE_ROOT')) { + $title = "Tentative d'accès non autorisée"; + $details = sprintf("L'utilisateur %s a tenté de consulter l'Administrateur ROOT protégé : %s %s (ID: %d)", + $this->getUser()->getUserIdentifier(), $account->getFirstName(), $account->getName(), $account->getId() + ); + $appLogger->record('SECURITY_ALERT', $details); + // $this->sendSecurityEmail($mailer, $title, $details); // Activez si la méthode existe + + $this->addFlash('error', 'Sécurité : Le compte est protégé'); + return $this->redirectToRoute('app_crm_administrateur'); + } + + // --- 1. LOGIQUE DE MISE À JOUR DU STATUT (Lien Direct) --- + // --- LOGIQUE DE MISE À JOUR DU STATUT (Lien Direct) --- +// --- LOGIQUE DE MISE À JOUR DU STATUT (Lien Direct) --- + if ($request->query->get('act') === 'updateStatut') { + $statusParam = $request->query->get('status'); + $newStatus = ($statusParam === 'true'); + + // On définit un sujet dynamique pour plus de clarté + $subject = $newStatus + ? "[Intranet Ludikevent] - Activation de votre accès" + : "[Intranet Ludikevent] - Suspension de votre accès"; + + // Envoi du mail de notification + $mailer->send( + $account->getEmail(), + $account->getFirstName() . " " . $account->getName(), + $subject, + "mails/account/status-update.twig", + [ + 'who' => $this->getUser(), + 'account' => $account, + 'stats' => $newStatus, + ], + [] + ); + + // Mise à jour en base de données + // Vérifiez si votre entité utilise setActif() ou setIsActif() + $account->setIsActif($newStatus); + $em->flush(); + + $label = $newStatus ? 'activé' : 'désactivé'; + + // Log et Message Flash + $appLogger->record('UPDATE_STATUS', sprintf("%s du compte pour l'admin %d", ucfirst($label), $account->getId())); + $this->addFlash('success', "Le compte de " . $account->getFirstName() . " a été $label avec succès."); + + return $this->redirectToRoute('app_crm_administrateur_view', ['id' => $account->getId()]); + } + + // --- 2. LOGIQUE DE MISE À JOUR DES RÔLES (POST) --- + if ($request->isMethod('POST') && $request->query->get('act') === 'updateRoles') { + $submittedRoles = $request->request->all('roles'); + $mandatoryRoles = ['ROLE_USER', 'ROLE_ADMIN']; + + foreach ($mandatoryRoles as $role) { + if (!in_array($role, $submittedRoles)) { + $submittedRoles[] = $role; + } + } + + if (in_array('ROLE_CLIENT_MAIN', $submittedRoles) && !$this->isGranted('ROLE_ROOT')) { + $this->addFlash('error', 'Action non autorisée : Privilège ROOT requis.'); + return $this->redirectToRoute('app_crm_administrateur_view', ['id' => $account->getId()]); + } + + $account->setRoles($submittedRoles); + $em->flush(); + + $appLogger->record('UPDATE_ROLES', sprintf("Mise à jour des rôles pour l'admin %d", $account->getId())); + $this->addFlash('success', 'Les permissions ont été mises à jour.'); + return $this->redirectToRoute('app_crm_administrateur_view', ['id' => $account->getId()]); + } + + // --- 3. LOGIQUE CHANGEMENT DE MOT DE PASSE --- + $passwordForm = $this->createForm(AccountPasswordType::class, $account); + $passwordForm->handleRequest($request); + + if ($passwordForm->isSubmitted() && $passwordForm->isValid()) { + // 1. On récupère le mot de passe en clair + $plainPassword = $passwordForm->get('password')->getData(); + + // 2. On hache pour la base de données + $hashedPassword = $passwordHasher->hashPassword($account, $plainPassword); + $account->setPassword($hashedPassword); + $em->flush(); + + // 3. Envoi du mail avec les bons paramètres + $mailer->send( + $account->getEmail(), + $account->getName() . " " . $account->getFirstName(), + "[Intranet Ludikevent] - Modification de votre mot de passe", + "mails/account/password-modify.twig", // Correction orthographe 'account' + [ + 'who' => $this->getUser(), + 'account' => $account, + 'password' => $plainPassword // On envoie le mot de passe en clair ici + ], + [] + ); + + $appLogger->record('UPDATE_PASSWORD', sprintf("Mot de passe modifié et envoyé pour l'admin %d", $account->getId())); + $this->addFlash('success', 'Le mot de passe a été mis à jour et envoyé à l\'utilisateur.'); + + return $this->redirectToRoute('app_crm_administrateur_view', ['id' => $account->getId()]); + } + + // --- 4. LOGIQUE DU FORMULAIRE D'IDENTITÉ --- + $form = $this->createForm(AccountType::class, $account); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $em->flush(); + $appLogger->record('UPDATE_IDENTITY', sprintf("Mise à jour identité : %s %s", $account->getFirstName(), $account->getName())); + $this->addFlash('success', 'Informations personnelles mises à jour.'); + return $this->redirectToRoute('app_crm_administrateur_view', ['id' => $account->getId()]); + } + + // --- 5. AFFICHAGE & PAGINATION --- + $appLogger->record('VIEW', sprintf("Consultation : %s %s", $account->getFirstName(), $account->getName())); + + $loginPagination = $paginator->paginate( + $accountLoginRegisterRepository->findBy(['account' => $account], ['loginAt' => 'DESC']), + $request->query->getInt('page', 1), + 10, + ['pageParameterName' => 'page'] + ); + + $auditPagination = $paginator->paginate( + $auditLogRepository->findBy(['account' => $account], ['actionAt' => 'DESC']), + $request->query->getInt('audit_page', 1), + 10, + ['pageParameterName' => 'audit_page'] ); return $this->render('dashboard/administrateur/view.twig', [ 'admin' => $account, + 'form' => $form->createView(), + 'passwordForm' => $passwordForm->createView(), + 'loginRegisters' => $loginPagination, + 'auditLogs' => $auditPagination ]); } + /** + * Modifier un administrateur + */ + /** * Supprimer un administrateur */ #[Route(path: '/crm/administrateur/delete/{id}', name: 'app_crm_administrateur_delete', options: ['sitemap' => false], methods: ['POST'])] public function administrateurDelete( EventDispatcherInterface $eventDispatcher, - ?Account $account, - Request $request, - AppLogger $appLogger, - EntityManagerInterface $entityManager - ): Response { + ?Account $account, + Request $request, + AppLogger $appLogger, + EntityManagerInterface $entityManager, + Mailer $mailer + ): Response + { if (!$account) { $this->addFlash('error', 'Administrateur introuvable.'); return $this->redirectToRoute('app_crm_administrateur'); } - if ($account === $this->getUser()) { - $this->addFlash('error', 'Vous ne pouvez pas supprimer votre propre compte.'); + // --- SÉCURITÉ : Interdiction suppression ROOT ou CLIENT_MAIN --- + // On vérifie si l'un des rôles protégés est présent dans le tableau des rôles du compte + $protectedRoles = ['ROLE_ROOT', 'ROLE_CLIENT_MAIN']; + $accountRoles = $account->getRoles(); + + // Si l'intersection entre les rôles du compte et les rôles protégés n'est pas vide + if (array_intersect($protectedRoles, $accountRoles)) { + $title = "Blocage suppression compte HAUTE-HIÉRARCHIE"; + $details = sprintf("Tentative de suppression de l'Administrateur protégé %s (Rôles: %s) par l'utilisateur %s", + $account->getEmail(), + implode(', ', $accountRoles), + $this->getUser()->getUserIdentifier() + ); + + $appLogger->record('SECURITY_CRITICAL', $details); + $this->sendSecurityEmail($mailer, $title, $details); + + $this->addFlash('error', 'Sécurité : Ce compte est protégé et ne peut pas être supprimé.'); return $this->redirectToRoute('app_crm_administrateur'); } - // Récupération du token CSRF (compatible Turbo) $token = $request->request->get('_token') ?? $request->query->get('_token'); if ($this->isCsrfTokenValid('delete' . $account->getId(), $token)) { $name = $account->getFirstName() . ' ' . $account->getName(); $email = $account->getEmail(); - // Audit Log : On trace avant la suppression pour garder les infos $appLogger->record( 'DELETE', - sprintf("Suppression définitive de l'administrateur : %s (%s)", $name, $email) + sprintf("Suppression définitive de l'Administrateur : %s (%s)", $name, $email) ); - // Dispatch pour notification mail $eventDispatcher->dispatch(new EventAdminDeleted($account, $this->getUser())); $entityManager->remove($account); $entityManager->flush(); - $this->addFlash('success', "Le compte de $name a été supprimé."); + $this->addFlash('success', "L'Administrateur $name a été supprimé."); } else { $this->addFlash('error', 'Jeton de sécurité invalide.'); } return $this->redirectToRoute('app_crm_administrateur'); } + + private function sendSecurityEmail(Mailer $mailer, string $message, string $content): void + { + $mailer->send( + 'notification@siteconseil.fr', + "Notification Intranet Ludikevent", + "[Intranet Ludikevent] - " . $message, + "mails/notification/security_violation.twig", + [ + 'message' => $message, + 'content' => $content, + 'account' => $this->getUser() ? $this->getUser()->getUserIdentifier() : 'Système', + 'date' => new \DateTime(), + ] + ); + } } diff --git a/src/Controller/Dashboard/LogsController.php b/src/Controller/Dashboard/LogsController.php index 977e1b8..773dc91 100644 --- a/src/Controller/Dashboard/LogsController.php +++ b/src/Controller/Dashboard/LogsController.php @@ -4,6 +4,7 @@ namespace App\Controller\Dashboard; use App\Entity\AuditLog; use App\Repository\AuditLogRepository; +use Doctrine\ORM\EntityManagerInterface; use Knp\Component\Pager\PaginatorInterface; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; @@ -12,6 +13,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; class LogsController extends AbstractController { @@ -41,18 +43,30 @@ class LogsController extends AbstractController { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->setTitle('Logs Intranet Ludikevent'); + $sheet->setTitle('Logs Audit'); - // En-têtes stylisés - $headers = ['Date', 'Heure', 'Admin', 'Email', 'Action', 'Message', 'URL', 'Navigateur/OS']; - foreach (range('A', 'G') as $i => $column) { + // 1. Génération d'un mot de passe aléatoire pour ce fichier + // On utilise bin2hex pour un mot de passe robuste + $ultraSecurePassword = bin2hex(random_bytes(32)); + + // En-têtes (A à J) + $headers = ['Date', 'Heure', 'Admin', 'Email', 'Action', 'Message', 'URL', 'Navigateur/OS', 'Signature', 'État']; + $columns = range('A', 'J'); + + foreach ($columns as $i => $column) { $sheet->setCellValue($column . '1', $headers[$i]); $sheet->getStyle($column . '1')->getFont()->setBold(true); + $sheet->getStyle($column . '1')->getFill() + ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID) + ->getStartColor()->setARGB('F1F5F9'); } $row = 2; /** @var AuditLog $log */ foreach ($logs as $log) { + $isValid = ($log->getHashCode() === $log->generateSignature()); + $statusText = $isValid ? 'VALIDE' : 'CORROMPU'; + $sheet->setCellValue('A' . $row, $log->getActionAt()->format('d/m/Y')); $sheet->setCellValue('B' . $row, $log->getActionAt()->format('H:i:s')); $sheet->setCellValue('C' . $row, $log->getAccount()->getFirstName() . ' ' . $log->getAccount()->getName()); @@ -61,16 +75,77 @@ class LogsController extends AbstractController $sheet->setCellValue('F' . $row, $log->getMessage()); $sheet->setCellValue('G' . $row, $log->getPath()); $sheet->setCellValue('H' . $row, $log->getUserAgent()); + $sheet->setCellValue('I' . $row, $log->getHashCode()); + $sheet->setCellValue('J' . $row, $statusText); + + // Couleur d'état + $color = $isValid ? '059669' : 'DC2626'; + $sheet->getStyle('J' . $row)->getFont()->setBold(true)->getColor()->setARGB($color); + $row++; } - $writer = new Xlsx($spreadsheet); - $response = new StreamedResponse(fn() => $writer->save('php://output')); + // Auto-ajustement des colonnes + foreach ($columns as $column) { + $sheet->getColumnDimension($column)->setAutoSize(true); + } + + // --- VERROUILLAGE DU FICHIER --- + + // On protège la feuille contre toute modification + $sheet->getProtection()->setPassword($ultraSecurePassword); + $sheet->getProtection()->setSheet(true); + + // On interdit la sélection des cellules verrouillées (optionnel) + $sheet->getProtection()->setSelectLockedCells(true); + $sheet->getProtection()->setSort(false); + $sheet->getProtection()->setInsertRows(false); + + // On verrouille la structure du classeur + $spreadsheet->getSecurity()->setLockStructure(true); + $spreadsheet->getSecurity()->setWorkbookPassword($ultraSecurePassword); + $spreadsheet->getProperties() + ->setCreator("Système Audit Ludikevent") + ->setLastModifiedBy("Automate") + ->setCompany("SARL SITECONSEIL") + ->setCategory("Système Audit Ludikevent") + ->setTitle("Journal d'audit protégé") + ->setSubject("Confidentiel") + ->setDescription("Ce fichier est protégé par signature numérique et verrouillage structurel."); + // --- GÉNÉRATION DE LA RÉPONSE --- + $writer = new Xlsx($spreadsheet); + + $response = new StreamedResponse(function() use ($writer) { + $writer->save('php://output'); + }); + + // On ajoute la date et un identifiant court dans le nom du fichier + $fileName = 'audit_ludikevent_' . date('d-m-Y_H-i') . '.xlsx'; - $fileName = 'export_logs_' . date('d_m_Y') . '.xlsx'; $response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); $response->headers->set('Content-Disposition', 'attachment;filename="' . $fileName . '"'); + $response->headers->set('Cache-Control', 'max-age=0'); return $response; } + #[Route(path: '/crm/logs/delete/{id}', name: 'app_crm_audit_logs_delete', methods: ['POST'])] + #[IsGranted('ROLE_ROOT')] + public function deleteLog( + AuditLog $log, + Request $request, + EntityManagerInterface $entityManager + ): Response { + // Vérification du token CSRF dynamique (lié à l'ID du log) + if (!$this->isCsrfTokenValid('delete' . $log->getId(), $request->request->get('_token'))) { + $this->addFlash('error', 'Action non autorisée.'); + return $this->redirectToRoute('app_crm_audit_logs'); + } + + $entityManager->remove($log); + $entityManager->flush(); + + $this->addFlash('success', 'L\'entrée du journal a été supprimée.'); + + return $this->redirectToRoute('app_crm_audit_logs'); + } } diff --git a/src/Entity/AuditLog.php b/src/Entity/AuditLog.php index 2227f5a..7d4143c 100644 --- a/src/Entity/AuditLog.php +++ b/src/Entity/AuditLog.php @@ -32,6 +32,8 @@ class AuditLog #[ORM\Column(length: 255)] private ?string $userAgent = null; + #[ORM\Column(length: 64)] + private ?string $hashCode = null; // Le constructeur force le remplissage des données dès le départ public function __construct(Account $account, string $type, string $message, string $path,string $userAgent) @@ -42,6 +44,22 @@ class AuditLog $this->path = $path; $this->actionAt = new \DateTimeImmutable(); $this->userAgent = $userAgent; + $this->hashCode = $this->generateSignature(); + } + + public function generateSignature(): string + { + $data = sprintf( + '%s|%s|%s|%s|%s|%s', + $this->account->getEmail(), // L'email est plus fiable que l'ID si l'user est nouveau + $this->type, + $this->message, + $this->path, + $this->actionAt->format('Y-m-d H:i:s'), + $_ENV['APP_SECRET'] ?? 'default_secret' + ); + + return hash('sha256', $data); } // Uniquement des Getters (Pas de Setters = Pas de modification possible en PHP) @@ -52,4 +70,5 @@ class AuditLog public function getMessage(): ?string { return $this->message; } public function getPath(): ?string { return $this->path; } public function getUserAgent(): ?string{ return $this->userAgent;} + public function getHashCode(): ?string{ return $this->hashCode;} } diff --git a/src/Form/AccountPasswordType.php b/src/Form/AccountPasswordType.php new file mode 100644 index 0000000..94e2896 --- /dev/null +++ b/src/Form/AccountPasswordType.php @@ -0,0 +1,56 @@ +add('password', RepeatedType::class, [ + 'type' => PasswordType::class, + 'invalid_message' => 'Les mots de passe doivent être identiques.', + 'required' => true, + 'first_options' => [ + 'label' => 'Nouveau mot de passe', + 'attr' => [ + 'placeholder' => 'Entrez le nouveau mot de passe', + 'class' => 'w-full px-4 py-3 bg-slate-50 dark:bg-slate-900 border-none rounded-xl focus:ring-2 focus:ring-indigo-500 transition-all text-sm' + ] + ], + 'second_options' => [ + 'label' => 'Confirmer le mot de passe', + 'attr' => [ + 'placeholder' => 'Répétez le mot de passe', + 'class' => 'w-full px-4 py-3 bg-slate-50 dark:bg-slate-900 border-none rounded-xl focus:ring-2 focus:ring-indigo-500 transition-all text-sm' + ] + ], + 'constraints' => [ + new NotBlank([ + 'message' => 'Veuillez entrer un mot de passe', + ]), + new Length([ + 'min' => 8, + 'minMessage' => 'Votre mot de passe doit contenir au moins {{ limit }} caractères', + 'max' => 4096, + ]), + ], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Account::class, + ]); + } +} diff --git a/src/Logger/AuditLogSecurityListener.php b/src/Logger/AuditLogSecurityListener.php index 297b430..769a016 100644 --- a/src/Logger/AuditLogSecurityListener.php +++ b/src/Logger/AuditLogSecurityListener.php @@ -4,20 +4,39 @@ namespace App\Logger; use App\Entity\AuditLog; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; +use Doctrine\ORM\Event\PrePersistEventArgs; use Doctrine\ORM\Events; use Doctrine\ORM\Event\PreUpdateEventArgs; +use Symfony\Bundle\SecurityBundle\Security; #[AsEntityListener(event: Events::preUpdate, method: 'preUpdate', entity: AuditLog::class)] #[AsEntityListener(event: Events::preRemove, method: 'preRemove', entity: AuditLog::class)] +#[AsEntityListener(event: Events::prePersist, method: 'prePersist', entity: AuditLog::class)] + class AuditLogSecurityListener { + public function __construct( + private Security $security + ) {} + + // Exécuté lors de la CRÉATION initiale du log + public function prePersist(AuditLog $log, PrePersistEventArgs $event): void + { + $log->generateSignature(); + } + public function preUpdate(AuditLog $log, PreUpdateEventArgs $event): void { - throw new \LogicException("AuditLog est immuable : modification interdite."); + if (!$this->security->isGranted('ROLE_ROOT')) { + throw new \LogicException("AuditLog est immuable : modification interdite."); + } + $log->generateSignature(); } public function preRemove(AuditLog $log): void { - throw new \LogicException("AuditLog est protégé : suppression interdite."); + if (!$this->security->isGranted('ROLE_ROOT')) { + throw new \LogicException("AuditLog est protégé : suppression interdite."); + } } } diff --git a/templates/dashboard/administrateur.twig b/templates/dashboard/administrateur.twig index aec10c4..69d710b 100644 --- a/templates/dashboard/administrateur.twig +++ b/templates/dashboard/administrateur.twig @@ -3,90 +3,142 @@ {% block title %}Administrateurs{% endblock %} {% block actions %} - {# Bouton Ajouter un administrateur #} - {% endblock %} {% block body %}
Gestion des accès et des niveaux d'accréditation système.
+| Utilisateur | -Statut | -Actions | -|||||
|---|---|---|---|---|---|---|---|
|
-
- {{ admin.firstName }} {{ admin.name }}
- @{{ admin.username }}
-
- |
- - {{ admin.email }} - | -- {% if admin.actif %} - - - Actif - - {% else %} - - - Suspendu - - {% endif %} - | -
- {# Bouton Voir #}
-
- |
- ||||
| - Aucun administrateur trouvé. - | +Identité de l'Administrateur | +Rôles & Habilitations | +Statut du compte | +Actions | |||
|---|---|---|---|---|---|---|---|
Aucun Administrateur enregistré
+Commencez par en ajouter un via le bouton en haut à droite.
+Données personnelles
+Le rang Client Principal accorde nativement toutes les permissions système.
+Historique des actions effectuées par cet utilisateur
+| Date & Heure | +IP | +
|---|---|
| {{ login.loginAt|date('d/m/Y H:i') }} | ++ + {{ login.ip }} + + | +
| Aucune connexion enregistrée. | |
Historique complet des interactions sur l'Intranet
+Historique sécurisé par signature cryptographique
- {{ log.path }}
-
+ {{ log.path }}