diff --git a/migrations/Version20260115183138.php b/migrations/Version20260115183138.php new file mode 100644 index 0000000..5ca66cd --- /dev/null +++ b/migrations/Version20260115183138.php @@ -0,0 +1,36 @@ +addSql('CREATE TABLE audit_log (id SERIAL NOT NULL, account_id INT DEFAULT NULL, action_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, type VARCHAR(255) NOT NULL, message VARCHAR(255) NOT NULL, path VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_F6E1C0F59B6B5FBA ON audit_log (account_id)'); + $this->addSql('COMMENT ON COLUMN audit_log.action_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE audit_log ADD CONSTRAINT FK_F6E1C0F59B6B5FBA FOREIGN KEY (account_id) REFERENCES "account" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + 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 CONSTRAINT FK_F6E1C0F59B6B5FBA'); + $this->addSql('DROP TABLE audit_log'); + } +} diff --git a/src/Controller/Dashboard/AccountController.php b/src/Controller/Dashboard/AccountController.php index c2f4229..d57acb6 100644 --- a/src/Controller/Dashboard/AccountController.php +++ b/src/Controller/Dashboard/AccountController.php @@ -3,57 +3,161 @@ namespace App\Controller\Dashboard; use App\Entity\Account; -use App\Entity\AccountResetPasswordRequest; +use App\Event\Object\EventAdminCreate; +use App\Event\Object\EventAdminDeleted; use App\Form\AccountType; -use App\Form\RequestPasswordConfirmType; -use App\Form\RequestPasswordRequestType; +use App\Logger\AppLogger; use App\Repository\AccountRepository; -use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent; -use App\Service\ResetPassword\Event\ResetPasswordEvent; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; -use Symfony\Contracts\HttpClient\HttpClientInterface; - +use Symfony\Component\Uid\Uuid; class AccountController extends AbstractController { - - #[Route(path: '/crm/administrateur', name: 'app_crm_administrateur', options: ['sitemap' => false], methods: ['GET','POST'])] - public function administrateur(AccountRepository $accountRepository): Response + /** + * 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 { - return $this->render('dashboard/administrateur.twig',[ + // Audit Log : On trace la consultation de la liste + $appLogger->record('VIEW', 'Consultation de la liste des administrateurs'); + $eventDispatcher->dispatch(new EventAdminCreate($accountRepository->findAdmin()[0], $this->getUser())); + return $this->render('dashboard/administrateur.twig', [ 'admins' => $accountRepository->findAdmin(), ]); } - #[Route(path: '/crm/administrateur/add', name: 'app_crm_administrateur_add', options: ['sitemap' => false], methods: ['GET','POST'])] - public function administrateurAdd(Request $request): Response - { + + /** + * Ajouter un administrateur + */ + #[Route(path: '/crm/administrateur/add', name: 'app_crm_administrateur_add', options: ['sitemap' => false], methods: ['GET', 'POST'])] + public function administrateurAdd( + Request $request, + EntityManagerInterface $entityManager, + UserPasswordHasherInterface $passwordHasher, + 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()); + $account->setRoles(['ROLE_ADMIN']); + $form = $this->createForm(AccountType::class, $account); $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { + if ($form->isSubmitted() && $form->isValid()) { + // Hachage du mot de passe temporaire + $tempPassword = bin2hex(random_bytes(20)); + $hashedPassword = $passwordHasher->hashPassword($account, $tempPassword); + $account->setPassword($hashedPassword); + + $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() + ) + ); + + // 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.'); + + return $this->redirectToRoute('app_crm_administrateur'); } + return $this->render('dashboard/administrateur/add.twig', [ 'form' => $form->createView(), ]); } - #[Route(path: '/crm/administrateur/{id}', name: 'app_crm_administrateur_view', options: ['sitemap' => false], methods: ['GET','POST'])] - public function administrateurView(?Account $account): Response - { + /** + * 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 + { + 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() + ) + ); + + return $this->render('dashboard/administrateur/view.twig', [ + 'admin' => $account, + ]); } - #[Route(path: '/crm/administrateur/delete/{id}', name: 'app_crm_administrateur_delete_view', options: ['sitemap' => false], methods: ['GET','POST'])] - public function administrateurDelete(?Account $account): Response - { + /** + * 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 { + 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.'); + 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) + ); + + // 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é."); + } else { + $this->addFlash('error', 'Jeton de sécurité invalide.'); + } + + return $this->redirectToRoute('app_crm_administrateur'); } } diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index fd41b9b..c535512 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -6,8 +6,10 @@ use App\Entity\Account; use App\Entity\AccountResetPasswordRequest; use App\Form\RequestPasswordConfirmType; use App\Form\RequestPasswordRequestType; +use App\Logger\AppLogger; use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent; use App\Service\ResetPassword\Event\ResetPasswordEvent; +use Doctrine\ORM\EntityManagerInterface; use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -121,6 +123,29 @@ class HomeController extends AbstractController 'account' => $account, ]); } + + #[Route('/unscribe/{email}', name: 'app_unscribe', options: ['sitemap' => false], methods: ['GET', 'POST'])] + public function appUnscribe( + string $email, + Request $request, + EntityManagerInterface $entityManager, + AppLogger $appLogger + ): Response { + // 1. Décodage de l'email (au cas où il y aurait des caractères spéciaux) + $email = urldecode($email); + + // 3. Gestion du POST (Désinscription en un clic via le client mail) + if ($request->isMethod('POST')) { + $appLogger->record('UNSUBSCRIBE', sprintf("Désinscription automatique (One-Click) de : %s", $email)); + return new JsonResponse(['status' => 'success'], Response::HTTP_OK); + } + + $appLogger->record('UNSUBSCRIBE', sprintf("Désinscription manuelle de : %s", $email)); + + return $this->render('security/unscribe_success.twig', [ + 'email' => $email + ]); + } const SENTRY_HOST = ''; const SENTRY_PROJECT_IDS = ['']; diff --git a/src/Entity/Account.php b/src/Entity/Account.php index 1526fb3..bb5817c 100644 --- a/src/Entity/Account.php +++ b/src/Entity/Account.php @@ -69,9 +69,16 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column(type: 'string', nullable: true)] private ?string $googleAuthenticatorSecret = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: AuditLog::class, mappedBy: 'account')] + private Collection $auditLogs; + public function __construct() { $this->accountLoginRegisters = new ArrayCollection(); + $this->auditLogs = new ArrayCollection(); } public function getId(): ?int @@ -313,4 +320,34 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface { $this->googleAuthenticatorSecret = $googleAuthenticatorSecret; } + + /** + * @return Collection + */ + public function getAuditLogs(): Collection + { + return $this->auditLogs; + } + + public function addAuditLog(AuditLog $auditLog): static + { + if (!$this->auditLogs->contains($auditLog)) { + $this->auditLogs->add($auditLog); + $auditLog->setAccount($this); + } + + return $this; + } + + public function removeAuditLog(AuditLog $auditLog): static + { + if ($this->auditLogs->removeElement($auditLog)) { + // set the owning side to null (unless already changed) + if ($auditLog->getAccount() === $this) { + $auditLog->setAccount(null); + } + } + + return $this; + } } diff --git a/src/Entity/AuditLog.php b/src/Entity/AuditLog.php new file mode 100644 index 0000000..06ae724 --- /dev/null +++ b/src/Entity/AuditLog.php @@ -0,0 +1,50 @@ +account = $account; + $this->type = $type; + $this->message = $message; + $this->path = $path; + $this->actionAt = new \DateTimeImmutable(); + } + + // Uniquement des Getters (Pas de Setters = Pas de modification possible en PHP) + public function getId(): ?int { return $this->id; } + public function getAccount(): ?Account { return $this->account; } + public function getActionAt(): \DateTimeImmutable { return $this->actionAt; } + public function getType(): ?string { return $this->type; } + public function getMessage(): ?string { return $this->message; } + public function getPath(): ?string { return $this->path; } +} diff --git a/src/Event/Object/EventAdminCreate.php b/src/Event/Object/EventAdminCreate.php new file mode 100644 index 0000000..ecc534f --- /dev/null +++ b/src/Event/Object/EventAdminCreate.php @@ -0,0 +1,30 @@ +account = $account; + $this->requestedAccount = $requestedAccount; + } + + /** + * @return Account + */ + public function getAccount(): Account + { + return $this->account; + } + + public function getRequestedAccount() + { + return $this->requestedAccount; + } +} diff --git a/src/Event/Object/EventAdminDeleted.php b/src/Event/Object/EventAdminDeleted.php new file mode 100644 index 0000000..badc7c1 --- /dev/null +++ b/src/Event/Object/EventAdminDeleted.php @@ -0,0 +1,30 @@ +account = $account; + $this->requestedAccount = $requestedAccount; + } + + /** + * @return Account + */ + public function getAccount(): Account + { + return $this->account; + } + + public function getRequestedAccount() + { + return $this->requestedAccount; + } +} diff --git a/src/Event/Service/AdminEvent.php b/src/Event/Service/AdminEvent.php new file mode 100644 index 0000000..8072f6b --- /dev/null +++ b/src/Event/Service/AdminEvent.php @@ -0,0 +1,106 @@ +getAccount(); + + // On cherche si une demande existe déjà pour cet utilisateur + $existingRequest = $this->entityManager->getRepository(AccountResetPasswordRequest::class) + ->findOneBy(['Account' => $account]); + + $now = new \DateTimeImmutable(); + $sendNewRequest = true; + $request = null; + + if ($existingRequest instanceof AccountResetPasswordRequest) { + if ($existingRequest->getExpiresAt() < $now) { + $this->entityManager->remove($existingRequest); + $this->entityManager->flush(); + } else { + $sendNewRequest = false; + $request = $existingRequest; + } + } + + if ($sendNewRequest) { + $expiredAt = $now->modify('+24 hours'); // On donne 24h pour une création de compte + + $request = new AccountResetPasswordRequest(); + $request->setAccount($account); + $request->setToken(TempPasswordGenerator::generate(50, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')); + $request->setRequestedAt($now); + $request->setExpiresAt($expiredAt); + + $this->entityManager->persist($request); + $this->entityManager->flush(); + } + + // Génération du lien de confirmation + $resetLink = $this->urlGenerator->generate( + 'app_forgot_password_confirm', + ['id' => $account->getId(), 'token' => $request->getToken()], + UrlGeneratorInterface::ABSOLUTE_URL + ); + + // Envoi du mail de bienvenue / activation + $this->mailer->send( + $account->getEmail(), + $account->getFirstName() . ' ' . $account->getName(), + "[Intranet Ludikevent] Activation de votre accès administrateur", + "mails/notification/admin_created.twig", + [ + 'admin' => $account, + 'creator' => $eventAdminCreate->getRequestedAccount(), + 'setup_url' => $resetLink, + 'expires_at' => $request->getExpiresAt(), + ] + ); + } + + public function onAdminDeleted(EventAdminDeleted $eventAdminDeleted): void + { + $context = [ + 'admin' => $eventAdminDeleted->getAccount(), + 'who' => $eventAdminDeleted->getRequestedAccount(), + 'date' => new \DateTimeImmutable(), + ]; + + $recipients = [ + 'notification@siteconseil.fr' => "Notification Intranet Ludikevent", + 'contact@ludikevent.fr' => "Notification Intranet Ludikevent" + ]; + + foreach ($recipients as $email => $name) { + $this->mailer->send( + $email, + $name, + "[Intranet Ludikevent] - Suppression d'un administrateur", + "mails/notification/admin_deleted.twig", + $context + ); + } + } +} diff --git a/src/Event/Service/LoginStatsSubscriber.php b/src/Event/Service/LoginStatsSubscriber.php new file mode 100644 index 0000000..7d69d97 --- /dev/null +++ b/src/Event/Service/LoginStatsSubscriber.php @@ -0,0 +1,39 @@ +getUser(); + + // On ne loggue que si c'est bien une instance de notre entité Account + if (!$user instanceof Account) { + return; + } + + $request = $event->getRequest(); + + $loginRegister = new AccountLoginRegister(); + $loginRegister->setAccount($user); + $loginRegister->setLoginAt(new \DateTimeImmutable()); + $loginRegister->setIp($request->getClientIp()); + + $loginRegister->setUserAgent($request->headers->get('User-Agent')); + + $this->entityManager->persist($loginRegister); + $this->entityManager->flush(); + } +} diff --git a/src/Logger/AppLogger.php b/src/Logger/AppLogger.php new file mode 100644 index 0000000..038d8ba --- /dev/null +++ b/src/Logger/AppLogger.php @@ -0,0 +1,35 @@ +security->getUser(); + if (!$user instanceof Account) { + return; + } + + $request = $this->requestStack->getCurrentRequest(); + $path = $request ? $request->getRequestUri() : 'CLI/Internal'; + + // Création de l'objet immuable via le constructeur + $log = new AuditLog($user, $type, $message, $path); + + $this->entityManager->persist($log); + $this->entityManager->flush(); + } +} diff --git a/src/Logger/AuditLogSecurityListener.php b/src/Logger/AuditLogSecurityListener.php new file mode 100644 index 0000000..297b430 --- /dev/null +++ b/src/Logger/AuditLogSecurityListener.php @@ -0,0 +1,23 @@ + + */ +class AuditLogRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, AuditLog::class); + } + + // /** + // * @return AuditLog[] Returns an array of AuditLog objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('a') + // ->andWhere('a.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('a.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?AuditLog + // { + // return $this->createQueryBuilder('a') + // ->andWhere('a.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Security/KeycloakAuthenticator.php b/src/Security/KeycloakAuthenticator.php index 4c75487..4c8addc 100644 --- a/src/Security/KeycloakAuthenticator.php +++ b/src/Security/KeycloakAuthenticator.php @@ -54,6 +54,7 @@ class KeycloakAuthenticator extends OAuth2Authenticator implements Authenticatio $existingUser = $this->entityManager->getRepository(Account::class)->findOneBy(['keycloakId' => $keycloakUser->getId()]); if ($existingUser) { + return $existingUser; } diff --git a/src/Service/Mailer/Mailer.php b/src/Service/Mailer/Mailer.php index f62ef63..27bae05 100644 --- a/src/Service/Mailer/Mailer.php +++ b/src/Service/Mailer/Mailer.php @@ -1,6 +1,5 @@ mailer = $mailer; } /** - * Convertit le contenu MJML en HTML via le binaire MJML installé sur le serveur. + * Convertit le contenu MJML en HTML via le binaire MJML. */ private function convertMjmlToHtml(string $mjmlContent): string { @@ -52,31 +50,24 @@ class Mailer } /** - * Envoie un email avec headers de désinscription et Message-ID personnalisé. - * * @param string $address Email du destinataire - * @param string $addressName Nom du destinataire - * @param string $subject Sujet de l'email - * @param string $template Chemin du template Twig/MJML - * @param array $data Données à passer au template - * @param array<\Symfony\Component\Mime\Part\DataPart> $files Pièces jointes + * Envoie un email avec headers de désinscription et Message-ID conforme RFC 2822. */ public function send( string $address, string $addressName, string $subject, string $template, - array $data, - array $files = [] - ): void - { + array $data, + array $files = [] + ): void { $domain = "ludikevent.fr"; $dest = new Address($address, $addressName); $src = new Address("contact@" . $domain, "Ludikevent"); - // 1. Génération du Message-ID unique - $messageId = sprintf('<%s.%s@%s>', - bin2hex(random_bytes(12)), + // 1. Génération du Message-ID (SANS les crochets < >, Symfony les ajoute) + $messageId = sprintf('%s.%s@%s', time(), + bin2hex(random_bytes(8)), $domain ); @@ -88,30 +79,30 @@ class Mailer ->to($dest) ->from($src); - // 3. Configuration des Headers techniques + // 3. Configuration des Headers $headers = $mail->getHeaders(); - // Identification unique + // addIdHeader entoure automatiquement la valeur de < > $headers->addIdHeader('Message-ID', $messageId); - // RFC 2369 & 8058 (Bouton désinscription dans le client mail) + // RFC 2369 & 8058 $headers->addTextHeader('List-Unsubscribe', sprintf(', <%s>', $domain, $unsubscribeUrl)); $headers->addTextHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click'); - // 4. Génération du contenu via Twig et MJML + // 4. Génération du contenu MJML $mjmlGenerator = $this->environment->render($template, [ 'system' => [ 'subject' => $subject, - 'path' => $_ENV['PATH_URL'] ?? 'https://intranet.ludikevent.fr', + 'path' => $_ENV['PATH_URL'] ?? 'https://ludikevent.fr', 'unsubscribe_url' => $unsubscribeUrl, - 'message_id' => $messageId + 'message_id' => $messageId // Ici on peut l'afficher pour info ], 'datas' => $data, ]); $htmlContent = $this->convertMjmlToHtml($mjmlGenerator); - // 5. Version Texte Brut (Fallback) + // 5. Fallback Texte Brut try { $txtContent = $this->environment->render('txt-' . $template, [ 'system' => [ @@ -123,21 +114,18 @@ class Mailer ]); $mail->text($txtContent); } catch (Exception) { - // Si le template TXT n'existe pas, on laisse le mailer gérer le fallback ou on ignore + // Optionnel : générer un texte brut basique si le template n'existe pas } - // 6. Ajout des pièces jointes foreach ($files as $file) { $mail->addPart($file); } $mail->html($htmlContent); - // 7. Envoi final try { $this->mailer->send($mail); } catch (TransportExceptionInterface $e) { - // Optionnel : Logger l'erreur ici throw $e; } } diff --git a/src/Service/ResetPassword/ResetPasswordSubscriber.php b/src/Service/ResetPassword/ResetPasswordSubscriber.php index 7efa766..659c2e0 100644 --- a/src/Service/ResetPassword/ResetPasswordSubscriber.php +++ b/src/Service/ResetPassword/ResetPasswordSubscriber.php @@ -71,7 +71,7 @@ class ResetPasswordSubscriber $this->mailer->send( $account->getEmail(), $account->getUsername(), - '[CRM] - Lien pour réinitialiser votre mot de passe', + '[Intranet Ludikevent] - Lien pour réinitialiser votre mot de passe', 'mails/reset.twig', [ 'account' => $account, diff --git a/templates/dashboard/administrateur.twig b/templates/dashboard/administrateur.twig index 0dc164a..aec10c4 100644 --- a/templates/dashboard/administrateur.twig +++ b/templates/dashboard/administrateur.twig @@ -19,7 +19,7 @@

Liste des Administrateurs

- {{ admins|length }} membres + {{ admins|length }} Administrateurs
@@ -67,7 +67,7 @@ {# Bouton Supprimer #} - diff --git a/templates/mails/notification/admin_created.twig b/templates/mails/notification/admin_created.twig new file mode 100644 index 0000000..a5ccbf8 --- /dev/null +++ b/templates/mails/notification/admin_created.twig @@ -0,0 +1,63 @@ +Voici le code MJML complet pour tes notifications d'administration. J'ai conçu un template élégant, responsive et aux couleurs de Ludikevent, incluant les deux variantes : la création (avec bouton d'activation) et la suppression. + +1. Template : mails/notification/admin_created.twig (MJML) +Ce template utilise les variables passées par ton service AdminEvent (datas.setup_url, datas.admin, etc.). + +XML + + + + [Intranet Ludikevent] Activation de compte + + + + + + + + .header-bg { background-color: #1a1a1a !important; } + .footer-text { font-size: 12px !important; color: #888888 !important; } + + + + + + + + LUDIKEVENT INTRANET + + + + + + + + Bienvenue, {{ datas.admin.firstName }} ! + + + Un accès administrateur vient d'être créé pour vous sur l'Intranet Ludikevent par {{ datas.creator.firstName }} {{ datas.creator.name }}. + + + Pour finaliser la configuration de votre compte et choisir votre mot de passe, veuillez cliquer sur le bouton ci-dessous : + + + ACTIVER MON COMPTE + + + Ce lien est valable pendant 24 heures (expire le {{ datas.expires_at|date('d/m/Y à H:i') }}). + + + + Si vous n'êtes pas à l'origine de cette demande, veuillez ignorer cet email. + + + + + + + + © {{ "now"|date("Y") }} Ludikevent - Système de notification automatique
+ Ne pas répondre à cet email. +
+
+
diff --git a/templates/mails/notification/admin_deleted.twig b/templates/mails/notification/admin_deleted.twig new file mode 100644 index 0000000..682e06a --- /dev/null +++ b/templates/mails/notification/admin_deleted.twig @@ -0,0 +1,38 @@ +{% extends 'mails/base.twig' %} + +{% block content %} + + + + Alerte de suppression de compte + + + + Une action de suppression définitive a été effectuée sur l'intranet LudikEvent. + + + + + Information + Détails + + + Compte supprimé + {{ datas.admin.firstName }} {{ datas.admin.name }} ({{ datas.admin.email }}) + + + Supprimé par + {{ datas.who.firstName }} {{ datas.who.name }} + + + Date et heure + {{ datas.date|date('d/m/Y à H:i') }} + + + + + Cette action est irréversible. Toutes les données associées à ce compte (historique de connexion, permissions spécifiques) ont été retirées de la base de données. + + + +{% endblock %} diff --git a/templates/security/forgot-password-confirm.twig b/templates/security/forgot-password-confirm.twig index 9cc56f1..17091af 100644 --- a/templates/security/forgot-password-confirm.twig +++ b/templates/security/forgot-password-confirm.twig @@ -72,4 +72,4 @@ -{% endblock % +{% endblock %} diff --git a/templates/security/unscribe_success.twig b/templates/security/unscribe_success.twig new file mode 100644 index 0000000..141cddd --- /dev/null +++ b/templates/security/unscribe_success.twig @@ -0,0 +1,37 @@ +{% extends 'base.twig' %} + +{% block title %}{{ 'email.removed'|trans }}{% endblock %} + +{% block body %} +
+
+ + {# Logo #} +
+ Ludikevent Logo +
+ + {# Icône de succès #} +
+ + + +
+ + {# Message #} +
+

+ {{ 'email.removed'|trans }} +

+

+ L'adresse email {{ email }} a bien été retirée de notre liste de diffusion. Vous ne recevrez plus de notifications automatiques de l'intranet. +

+
+ + +

+ © {{ "now"|date("Y") }} Ludikevent. Tous droits réservés. +

+
+
+{% endblock %} diff --git a/translations/messages.fr.yaml b/translations/messages.fr.yaml index 41c4ec5..900c825 100644 --- a/translations/messages.fr.yaml +++ b/translations/messages.fr.yaml @@ -29,3 +29,4 @@ logout_link: Déconnexion page.login: Connexion logged_admin: Administration button.sso: Connexion SSO +email.removed: Email supprimée