From b1b2687320f4c0ab5476d32b40bd5cb558832ab3 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Thu, 15 Jan 2026 18:51:17 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ansible):=20Ajoute=20des=20hea?= =?UTF-8?q?ders=20de=20s=C3=A9curit=C3=A9=20et=20limite=20la=20taille=20de?= =?UTF-8?q?s=20requ=C3=AAtes.=20=E2=9C=A8=20feat(Security):=20Active=20l'a?= =?UTF-8?q?uthentification=20=C3=A0=20deux=20facteurs=20(2FA).=20=E2=9C=A8?= =?UTF-8?q?=20feat(Account):=20Ajoute=20une=20entit=C3=A9=20et=20un=20form?= =?UTF-8?q?ulaire=20pour=20les=20administrateurs.=20=F0=9F=90=9B=20fix(Sec?= =?UTF-8?q?urity):=20Corrige=20la=20redirection=20apr=C3=A8s=20la=20connex?= =?UTF-8?q?ion.=20=E2=9C=A8=20feat(CRM):=20Ajoute=20une=20page=20d'adminis?= =?UTF-8?q?tration=20des=20comptes=20administrateurs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- ansible/templates/caddy.j2 | 13 +- assets/admin.js | 77 ++++++ assets/admin.scss | 10 + composer.json | 4 +- composer.lock | 193 ++++++++++++++- config/bundles.php | 1 + config/packages/scheb_2fa.yaml | 5 + config/packages/security.yaml | 49 ++-- config/routes.yaml | 7 + config/routes/scheb_2fa.yaml | 7 + migrations/Version20260115174840.php | 32 +++ .../Dashboard/AccountController.php | 59 +++++ src/Controller/Dashboard/HomeController.php | 9 - src/Entity/Account.php | 51 ++-- src/Form/AccountType.php | 40 ++++ src/Repository/AccountRepository.php | 15 ++ src/Security/AuthenticationEntryPoint.php | 2 +- symfony.lock | 13 ++ templates/dashboard/administrateur.twig | 94 +++++++- templates/dashboard/administrateur/add.twig | 109 +++++++++ templates/dashboard/base.twig | 219 ++++++------------ 22 files changed, 813 insertions(+), 198 deletions(-) create mode 100644 config/packages/scheb_2fa.yaml create mode 100644 config/routes/scheb_2fa.yaml create mode 100644 migrations/Version20260115174840.php create mode 100644 src/Controller/Dashboard/AccountController.php create mode 100644 src/Form/AccountType.php create mode 100644 templates/dashboard/administrateur/add.twig diff --git a/.env b/.env index 24cea0d..3e6127d 100644 --- a/.env +++ b/.env @@ -64,7 +64,7 @@ GOOGLE_APPLICATION_CREDENTIALS=%kernel.project_dir%/google.json ###< google/apiclient ### ###> sentry/sentry-symfony ### -SENTRY_DSN="" +SENTRY_DSN="https://5c3a5a5e27365d3db5ca866129ff7600@sentry.esy-web.dev/23" ###< sentry/sentry-symfony ### DEFAULT_URI=https://esyweb.local KEYCLOAK_AUTH_SERVER_URL=https://auth.esy-web.dev diff --git a/ansible/templates/caddy.j2 b/ansible/templates/caddy.j2 index d5056d5..40ab426 100644 --- a/ansible/templates/caddy.j2 +++ b/ansible/templates/caddy.j2 @@ -2,14 +2,25 @@ intranet.ludikevent.fr { tls { dns cloudflare KL6pZ-Z_12_zbnM2TtFDIsKM8A-HLPhU5GJJbKTW } - root * {{ path }}/public + root * {{ path }}/public file_server + request_body { max_size 100MB } + header { + # This prevents search engines from indexing the site + X-Robots-Tag "noindex, nofollow, nosnippet, noarchive" + + # Your existing Permissions-Policy Permissions-Policy "accelerometer=(), autoplay=(), camera=(), clipboard-write=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), usb=(), vr=(), screen-wake-lock=(), xr-spatial-tracking=(), bluetooth=(), ambient-light-sensor=(), battery=(), gamepad=(), notifications=(), push=()" + + # Recommended security headers for an intranet + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" } php_fastcgi unix//run/php/php8.3-fpm.sock { diff --git a/assets/admin.js b/assets/admin.js index 666d5b2..f4ebf60 100644 --- a/assets/admin.js +++ b/assets/admin.js @@ -1,2 +1,79 @@ import './admin.scss' import * as Turbo from "@hotwired/turbo" + +// Cette fonction initialise tous les écouteurs d'événements +function initAdminLayout() { + const sidebar = document.getElementById('sidebar'); + const overlay = document.getElementById('sidebar-overlay'); + const toggleBtn = document.getElementById('sidebar-toggle'); + const settingsToggle = document.getElementById('settings-toggle'); + const settingsSubmenu = document.getElementById('settings-submenu'); + const settingsChevron = document.getElementById('settings-chevron'); + + // --- TOGGLE SIDEBAR MOBILE --- + if (toggleBtn && sidebar && overlay) { + // On clone pour éviter de doubler les events avec Turbo + toggleBtn.replaceWith(toggleBtn.cloneNode(true)); + const newToggleBtn = document.getElementById('sidebar-toggle'); + + newToggleBtn.addEventListener('click', () => { + sidebar.classList.toggle('-translate-x-full'); + overlay.classList.toggle('hidden'); + }); + + overlay.addEventListener('click', () => { + sidebar.classList.add('-translate-x-full'); + overlay.classList.add('hidden'); + }); + } + + // --- GESTION SOUS-MENU PARAMÈTRES --- + if (settingsToggle) { + settingsToggle.replaceWith(settingsToggle.cloneNode(true)); + const newSettingsToggle = document.getElementById('settings-toggle'); + + newSettingsToggle.addEventListener('click', (e) => { + e.preventDefault(); + if (settingsSubmenu) settingsSubmenu.classList.toggle('hidden'); + if (settingsChevron) settingsChevron.classList.toggle('rotate-180'); + }); + + // Persistance : Garder ouvert si on est dans une sous-route admin + if (window.location.pathname.includes('administrateur')) { + if (settingsSubmenu) settingsSubmenu.classList.remove('hidden'); + if (settingsChevron) settingsChevron.classList.add('rotate-180'); + } + } + + // --- GESTION DES MESSAGES FLASH (Auto-suppression 10s) --- + const flashes = document.querySelectorAll('.flash-message'); + flashes.forEach((flash) => { + // Supprime le message après 10 secondes + setTimeout(() => { + // Animation de sortie + flash.classList.add('opacity-0', 'translate-x-10'); + // Retrait du DOM après l'animation + setTimeout(() => flash.remove(), 500); + }, 10000); + }); +} + +// --- CORRECTIF DATA-TURBO-CONFIRM --- +// Force l'affichage de la confirmation native sur les liens avec data-turbo-confirm +document.addEventListener("turbo:click", (event) => { + const message = event.target.getAttribute("data-turbo-confirm"); + if (message && !confirm(message)) { + event.preventDefault(); + } +}); + +// S'exécute au premier chargement ET à chaque navigation Turbo +document.addEventListener('turbo:load', initAdminLayout); + +// Fermer la sidebar mobile avant que Turbo ne mette en cache la page +document.addEventListener('turbo:before-cache', () => { + const sidebar = document.getElementById('sidebar'); + const overlay = document.getElementById('sidebar-overlay'); + if (sidebar) sidebar.classList.add('-translate-x-full'); + if (overlay) overlay.classList.add('hidden'); +}); diff --git a/assets/admin.scss b/assets/admin.scss index f1d8c73..ed69e06 100644 --- a/assets/admin.scss +++ b/assets/admin.scss @@ -1 +1,11 @@ @import "tailwindcss"; + + +form { + label { + color: white !important; + } +} + +.animate-fadeIn { animation: fadeIn 0.3s ease-in-out; } +@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } diff --git a/composer.json b/composer.json index d59e937..1500612 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,9 @@ "phpoffice/phpspreadsheet": ">=5.4", "phpstan/phpdoc-parser": "^2.3.1", "presta/sitemap-bundle": "^4.2", - "sentry/sentry-symfony": "^5.8.3", + "scheb/2fa-bundle": "^7.13", + "scheb/2fa-google-authenticator": "^7.13", + "sentry/sentry-symfony": "^5.8", "setasign/fpdi": "^2.6.4", "spatie/mjml-php": "^1.2.5", "stancer/stancer": ">=2.0.1", diff --git a/composer.lock b/composer.lock index f609957..c494ddf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bd9ebbc9c455efb1ac19197eaf03b368", + "content-hash": "4bb28dc935f256091e8b6e96cd1fbfa3", "packages": [ { "name": "async-aws/core", @@ -7862,6 +7862,127 @@ ], "time": "2025-12-02T15:19:04+00:00" }, + { + "name": "scheb/2fa-bundle", + "version": "v7.13.1", + "source": { + "type": "git", + "url": "https://github.com/scheb/2fa-bundle.git", + "reference": "edcc14456b508aab37ec792cfc36793d04226784" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/edcc14456b508aab37ec792cfc36793d04226784", + "reference": "edcc14456b508aab37ec792cfc36793d04226784", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "symfony/config": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/event-dispatcher": "^6.4 || ^7.0", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/property-access": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/twig-bundle": "^6.4 || ^7.0" + }, + "conflict": { + "scheb/two-factor-bundle": "*" + }, + "suggest": { + "scheb/2fa-backup-code": "Emergency codes when you have no access to other methods", + "scheb/2fa-email": "Send codes by email", + "scheb/2fa-google-authenticator": "Google Authenticator support", + "scheb/2fa-totp": "Temporary one-time password (TOTP) support (Google Authenticator compatible)", + "scheb/2fa-trusted-device": "Trusted devices support" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Scheb\\TwoFactorBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Scheb", + "email": "me@christianscheb.de" + } + ], + "description": "A generic interface to implement two-factor authentication in Symfony applications", + "homepage": "https://github.com/scheb/2fa", + "keywords": [ + "2fa", + "Authentication", + "symfony", + "two-factor", + "two-step" + ], + "support": { + "source": "https://github.com/scheb/2fa-bundle/tree/v7.13.1" + }, + "time": "2025-12-18T15:29:07+00:00" + }, + { + "name": "scheb/2fa-google-authenticator", + "version": "v7.13.1", + "source": { + "type": "git", + "url": "https://github.com/scheb/2fa-google-authenticator.git", + "reference": "7ad34bbde343a0770571464127ee072aacb70a58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/scheb/2fa-google-authenticator/zipball/7ad34bbde343a0770571464127ee072aacb70a58", + "reference": "7ad34bbde343a0770571464127ee072aacb70a58", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "scheb/2fa-bundle": "self.version", + "spomky-labs/otphp": "^11.0" + }, + "suggest": { + "symfony/validator": "Needed if you want to use the Google Authenticator TOTP validator constraint" + }, + "type": "library", + "autoload": { + "psr-4": { + "Scheb\\TwoFactorBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Scheb", + "email": "me@christianscheb.de" + } + ], + "description": "Extends scheb/2fa-bundle with two-factor authentication using Google Authenticator", + "homepage": "https://github.com/scheb/2fa", + "keywords": [ + "2fa", + "Authentication", + "google-authenticator", + "symfony", + "two-factor", + "two-step" + ], + "support": { + "source": "https://github.com/scheb/2fa-google-authenticator/tree/v7.13.1" + }, + "time": "2025-12-04T15:55:14+00:00" + }, { "name": "sentry/sentry", "version": "4.19.1", @@ -8321,6 +8442,76 @@ ], "time": "2025-11-13T13:00:34+00:00" }, + { + "name": "spomky-labs/otphp", + "version": "11.4.1", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/otphp.git", + "reference": "126c99b6cbbc18992cf3fba3b87931ba4e312482" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/126c99b6cbbc18992cf3fba3b87931ba4e312482", + "reference": "126c99b6cbbc18992cf3fba3b87931ba4e312482", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^2.0 || ^3.0", + "php": ">=8.1", + "psr/clock": "^1.0", + "symfony/deprecation-contracts": "^3.2" + }, + "require-dev": { + "symfony/error-handler": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "OTPHP\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/otphp/contributors" + } + ], + "description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator", + "homepage": "https://github.com/Spomky-Labs/otphp", + "keywords": [ + "FreeOTP", + "RFC 4226", + "RFC 6238", + "google authenticator", + "hotp", + "otp", + "totp" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/otphp/issues", + "source": "https://github.com/Spomky-Labs/otphp/tree/11.4.1" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-01-05T13:20:36+00:00" + }, { "name": "spomky-labs/pki-framework", "version": "1.4.1", diff --git a/config/bundles.php b/config/bundles.php index 77def6c..027e6ae 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -19,4 +19,5 @@ return [ Sentry\SentryBundle\SentryBundle::class => ['prod' => true], Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true], KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true], + Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true], ]; diff --git a/config/packages/scheb_2fa.yaml b/config/packages/scheb_2fa.yaml new file mode 100644 index 0000000..8a33ebb --- /dev/null +++ b/config/packages/scheb_2fa.yaml @@ -0,0 +1,5 @@ +# See the configuration reference at https://symfony.com/bundles/SchebTwoFactorBundle/6.x/configuration.html +scheb_two_factor: + security_tokens: + - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken + - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 98fd2e3..9fe36dc 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,14 +1,9 @@ -# config/packages/security.yaml security: - # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: - # Appelle votre provider d'utilisateurs. - # Ici, nous configurons un provider d'entité pour notre classe Account, - # en spécifiant 'username' comme propriété d'identification. app_account_provider: entity: class: App\Entity\Account - property: email # Utilise le champ 'username' de votre entité Account pour l'authentification + property: email firewalls: dev: @@ -17,32 +12,40 @@ security: main: lazy: true - provider: app_account_provider # Utilise le provider que nous avons défini ci-dessus + provider: app_account_provider user_checker: App\Security\UserChecker + + # --- AJOUT DE LA CONFIGURATION 2FA --- + two_factor: + auth_form_path: 2fa_login # Route d'affichage du formulaire + check_path: 2fa_login_check # Route de soumission du code + # ------------------------------------- + form_login: - login_path: app_home # La route vers votre formulaire de connexion (GET) - check_path: app_home # L'URL où le formulaire POST sera soumis - enable_csrf: true # Active la protection CSRF - csrf_token_id: authenticate # ID du jeton CSRF (doit correspondre à celui dans votre Twig) + login_path: app_home + check_path: app_home + enable_csrf: true + csrf_token_id: authenticate + entry_point: App\Security\AuthenticationEntryPoint + custom_authenticator: - App\Security\LoginFormAuthenticator - App\Security\KeycloakAuthenticator - logout: - target: app_logout - # Configuration des algorithmes de hachage des mots de passe. - # Symfony choisira automatiquement le meilleur algorithme par défaut si non spécifié, - # mais vous pouvez le configurer explicitement. + logout: + path: app_logout # Assurez-vous d'utiliser 'path' + target: app_home + password_hashers: - App\Entity\Account: 'auto' # 'auto' sélectionne le meilleur algorithme disponible (recommandé) - # Ou pour spécifier bcrypt explicitement : - # App\Entity\Account: - # algorithm: bcrypt + App\Entity\Account: 'auto' role_hierarchy: - ROLE_ROOT: [ROLE_ADMIN] # + ROLE_ROOT: [ROLE_ADMIN] access_control: - - { path: ^/admin, roles: [ROLE_ADMIN] } - - { path: ^/, roles: PUBLIC_ACCESS } # Toutes les autres pages nécessitent une authentification complète + # Permettre l'accès aux pages 2FA même si on n'est pas encore pleinement "ROLE_ADMIN" + - { path: ^/2fa, roles: PUBLIC_ACCESS } + + - { path: ^/crm, roles: [ROLE_ADMIN] } + - { path: ^/, roles: PUBLIC_ACCESS } diff --git a/config/routes.yaml b/config/routes.yaml index ee969be..c14c284 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -7,3 +7,10 @@ controllers: presta_sitemap: resource: "@PrestaSitemapBundle/config/routing.yml" + +2fa_login: + path: /2fa + controller: scheb_two_factor.form_controller::form + +2fa_login_check: + path: /2fa_check diff --git a/config/routes/scheb_2fa.yaml b/config/routes/scheb_2fa.yaml new file mode 100644 index 0000000..9a8ca66 --- /dev/null +++ b/config/routes/scheb_2fa.yaml @@ -0,0 +1,7 @@ +2fa_login: + path: /2fa + defaults: + _controller: "scheb_two_factor.form_controller::form" + +2fa_login_check: + path: /2fa_check diff --git a/migrations/Version20260115174840.php b/migrations/Version20260115174840.php new file mode 100644 index 0000000..1e18655 --- /dev/null +++ b/migrations/Version20260115174840.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE account ADD google_authenticator_secret VARCHAR(255) DEFAULT 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 "account" DROP google_authenticator_secret'); + } +} diff --git a/src/Controller/Dashboard/AccountController.php b/src/Controller/Dashboard/AccountController.php new file mode 100644 index 0000000..c2f4229 --- /dev/null +++ b/src/Controller/Dashboard/AccountController.php @@ -0,0 +1,59 @@ + false], methods: ['GET','POST'])] + public function administrateur(AccountRepository $accountRepository): Response + { + 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 + { + $account = new Account(); + $account->setIsFirstLogin(true); + $account->setIsActif(false); + $form = $this->createForm(AccountType::class, $account); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + + } + 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 + { + + } + #[Route(path: '/crm/administrateur/delete/{id}', name: 'app_crm_administrateur_delete_view', options: ['sitemap' => false], methods: ['GET','POST'])] + public function administrateurDelete(?Account $account): Response + { + + } +} diff --git a/src/Controller/Dashboard/HomeController.php b/src/Controller/Dashboard/HomeController.php index 0127c25..91f12a9 100644 --- a/src/Controller/Dashboard/HomeController.php +++ b/src/Controller/Dashboard/HomeController.php @@ -28,13 +28,4 @@ class HomeController extends AbstractController { return $this->render('dashboard/home.twig'); } - - - #[Route(path: '/crm/administrateur', name: 'app_crm_administrateur', options: ['sitemap' => false], methods: ['GET','POST'])] - public function administrateur(AccountRepository $accountRepository): Response - { - return $this->render('dashboard/administrateur.twig',[ - 'admins' => $accountRepository->findAll(), - ]); - } } diff --git a/src/Entity/Account.php b/src/Entity/Account.php index 46d3dd2..1526fb3 100644 --- a/src/Entity/Account.php +++ b/src/Entity/Account.php @@ -18,7 +18,7 @@ use Symfony\Component\Security\Core\User\UserInterface; #[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_USERNAME', fields: ['username'])] #[UniqueEntity(fields: ['email'], message: 'Cette adresse e-mail est déjà utilisée.')] #[UniqueEntity(fields: ['uuid'], message: 'Cet identifiant unique (UUID) est déjà utilisé.')] -class Account implements UserInterface, PasswordAuthenticatedUserInterface, \Serializable +class Account implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue] @@ -66,6 +66,8 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface, \Ser #[ORM\Column(length: 255, nullable: true)] private ?string $name = null; + #[ORM\Column(type: 'string', nullable: true)] + private ?string $googleAuthenticatorSecret = null; public function __construct() { @@ -193,22 +195,24 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface, \Ser } - public function serialize() + public function __serialize(): array { - return serialize(array( - $this->id, - $this->email, - $this->username, - )); + return [ + 'id' => $this->id, + 'email' => $this->email, + 'username' => $this->username, + 'googleAuthenticatorSecret' => $this->googleAuthenticatorSecret, + 'uuid' => $this->uuid, + ]; } - public function unserialize(string $data) + public function __unserialize(array $data): void { - list ( - $this->id, - $this->email, - $this->username, - ) = unserialize($data); + $this->id = $data['id'] ?? null; + $this->email = $data['email'] ?? null; + $this->username = $data['username'] ?? null; + $this->googleAuthenticatorSecret = $data['googleAuthenticatorSecret'] ?? null; + $this->uuid = $data['uuid'] ?? null; } /** @@ -288,4 +292,25 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface, \Ser return $this; } + + public function isGoogleAuthenticatorEnabled(): bool + { + // La 2FA est active si l'utilisateur a un secret généré + return null !== $this->googleAuthenticatorSecret; + } + + public function getGoogleAuthenticatorUsername(): string + { + return $this->username; + } + + public function getGoogleAuthenticatorSecret(): ?string + { + return $this->googleAuthenticatorSecret; + } + + public function setGoogleAuthenticatorSecret(?string $googleAuthenticatorSecret): void + { + $this->googleAuthenticatorSecret = $googleAuthenticatorSecret; + } } diff --git a/src/Form/AccountType.php b/src/Form/AccountType.php new file mode 100644 index 0000000..0462156 --- /dev/null +++ b/src/Form/AccountType.php @@ -0,0 +1,40 @@ +add('username',TextType::class,[ + 'label' => 'Utilisateur', + 'required' => true, + ]) + ->add('email',EmailType::class,[ + 'label' => 'Email', + 'required' => true, + ]) + ->add('firstName',TextType::class,[ + 'label' => 'Nom', + 'required' => true, + ]) + ->add('name',TextType::class,[ + 'label' => 'Nom', + 'required' => true, + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('data_class',Account::class); + } +} diff --git a/src/Repository/AccountRepository.php b/src/Repository/AccountRepository.php index 0fba614..421c92b 100644 --- a/src/Repository/AccountRepository.php +++ b/src/Repository/AccountRepository.php @@ -33,7 +33,22 @@ class AccountRepository extends ServiceEntityRepository implements PasswordUpgra $this->getEntityManager()->flush(); } + /** + * @return Account[] + */ + public function findAdmin(): array + { + $entityManager = $this->getEntityManager(); + $sql = 'SELECT * FROM account WHERE roles::text LIKE :role ORDER BY name ASC'; + $rsm = new \Doctrine\ORM\Query\ResultSetMappingBuilder($entityManager); + $rsm->addRootEntityFromClassMetadata(Account::class, 'a'); + + $query = $entityManager->createNativeQuery($sql, $rsm); + $query->setParameter('role', '%"ROLE_ADMIN"%'); + + return $query->getResult(); + } } diff --git a/src/Security/AuthenticationEntryPoint.php b/src/Security/AuthenticationEntryPoint.php index bae4862..bace209 100644 --- a/src/Security/AuthenticationEntryPoint.php +++ b/src/Security/AuthenticationEntryPoint.php @@ -51,6 +51,6 @@ class AuthenticationEntryPoint implements AuthenticationEntryPointInterface ); } - return new RedirectResponse($this->urlGenerator->generate('app_login')); + return new RedirectResponse($this->urlGenerator->generate('app_home')); } } diff --git a/symfony.lock b/symfony.lock index 5a428db..65a8842 100644 --- a/symfony.lock +++ b/symfony.lock @@ -130,6 +130,19 @@ "presta/sitemap-bundle": { "version": "v4.2.0" }, + "scheb/2fa-bundle": { + "version": "7.13", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.0", + "ref": "1e6f68089146853a790b5da9946fc5974f6fcd49" + }, + "files": [ + "config/packages/scheb_2fa.yaml", + "config/routes/scheb_2fa.yaml" + ] + }, "sentry/sentry-symfony": { "version": "5.8", "recipe": { diff --git a/templates/dashboard/administrateur.twig b/templates/dashboard/administrateur.twig index 385aba3..0dc164a 100644 --- a/templates/dashboard/administrateur.twig +++ b/templates/dashboard/administrateur.twig @@ -1,5 +1,93 @@ {% extends 'dashboard/base.twig' %} -{% block title %}Administrateur{% endblock %} -{% block body %} - {{ dump(admins) }} + +{% block title %}Administrateurs{% endblock %} + +{% block actions %} + {# Bouton Ajouter un administrateur #} + + + + + Ajouter un administrateur + +{% endblock %} + +{% block body %} +
+
+
+

Liste des Administrateurs

+ + {{ admins|length }} membres + +
+ +
+ + + + + + + + + + + {% for admin in admins %} + + + + + + + {% else %} + + + + {% endfor %} + +
UtilisateurEmailStatutActions
+
+ {{ admin.firstName }} {{ admin.name }} + @{{ admin.username }} +
+
+ {{ admin.email }} + + {% if admin.actif %} + + + Actif + + {% else %} + + + Suspendu + + {% endif %} + + {# Bouton Voir #} + + + Gérer + + + {# Bouton Supprimer #} + + + + + Supprimer + +
+ Aucun administrateur trouvé. +
+
+
+
{% endblock %} diff --git a/templates/dashboard/administrateur/add.twig b/templates/dashboard/administrateur/add.twig new file mode 100644 index 0000000..e2a5166 --- /dev/null +++ b/templates/dashboard/administrateur/add.twig @@ -0,0 +1,109 @@ +{% extends 'dashboard/base.twig' %} + +{% block title %}Nouvel Administrateur{% endblock %} + +{% block actions %} + {# Bouton de retour vers la liste #} + + + + + Retour à la liste + +{% endblock %} + +{% block body %} +
+
+ +
+ + {# Bandeau d'information sur la procédure d'activation par email #} + + +
+ {{ form_start(form, {'attr': {'class': 'space-y-8'}}) }} + + {# Grille de formulaire en 2 colonnes sur desktop #} +
+ + {# Bloc IDENTIFIANT - Correction couleur Label dark:text-gray-200 #} +
+ {{ form_label(form.username, 'Identifiant (Username)', { + 'label_attr': {'class': 'block text-sm font-semibold text-gray-700 dark:text-gray-200'} + }) }} + {{ form_widget(form.username, { + 'attr': { + 'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400', + 'placeholder': 'Ex: jdoe' + } + }) }} +
{{ form_errors(form.username) }}
+
+ + {# Bloc EMAIL #} +
+ {{ form_label(form.email, 'Adresse mail professionnelle', { + 'label_attr': {'class': 'block text-sm font-semibold text-gray-700 dark:text-gray-200'} + }) }} + {{ form_widget(form.email, { + 'attr': { + 'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400', + 'placeholder': 'email@ludikevent.fr' + } + }) }} +
{{ form_errors(form.email) }}
+
+ + {# Bloc PRENOM #} +
+ {{ form_label(form.firstName, 'Prénom', { + 'label_attr': {'class': 'block text-sm font-semibold text-gray-700 dark:text-gray-200'} + }) }} + {{ form_widget(form.firstName, { + 'attr': { + 'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white' + } + }) }} +
{{ form_errors(form.firstName) }}
+
+ + {# Bloc NOM #} +
+ {{ form_label(form.name, 'Nom de famille', { + 'label_attr': {'class': 'block text-sm font-semibold text-gray-700 dark:text-gray-200'} + }) }} + {{ form_widget(form.name, { + 'attr': { + 'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white' + } + }) }} +
{{ form_errors(form.name) }}
+
+ +
+ + {# Pied de formulaire avec bouton d'action #} +
+ +
+ + {{ form_end(form) }} +
+
+
+
+{% endblock %} diff --git a/templates/dashboard/base.twig b/templates/dashboard/base.twig index 7e37fb5..153e016 100644 --- a/templates/dashboard/base.twig +++ b/templates/dashboard/base.twig @@ -3,181 +3,110 @@ - Tableau de Bord Administratif - - {{ vite_asset('admin.js',{}) }} - - + {% block title %}Administration{% endblock %} - LudikEvent + {{ vite_asset('admin.js', {}) }} - -
- + + +
+ + {# Overlay pour mobile #} + + + {# SIDEBAR #} - -
- -
+ {# CONTENU PRINCIPAL #} +
- - + +
+ +
+ {# Zone Flash Messages #} +
+ {% for label, messages in app.flashes %} + {% for message in messages %} +
+

{{ message }}

+ +
+ {% endfor %} + {% endfor %} +
-
-

- {% block title %}{% endblock %} -

-
- {% block actions %}{% endblock %} + {# Zone Contenu #} +
+
+

+ {% block title_header %}{{ block('title') }}{% endblock %} +

+
+ {% block actions %}{% endblock %} +
+
+ +
+ {% block body %}{% endblock %}
- {% block body %}{% endblock %} -
- -