diff --git a/assets/etl.js b/assets/etl.js new file mode 100644 index 0000000..0cdd4b7 --- /dev/null +++ b/assets/etl.js @@ -0,0 +1,3 @@ +import './etl.scss'; + +console.log('ETL Mobile Loaded'); diff --git a/assets/etl.scss b/assets/etl.scss new file mode 100644 index 0000000..fa31e7d --- /dev/null +++ b/assets/etl.scss @@ -0,0 +1,10 @@ +@import "tailwindcss"; + +body { + @apply bg-slate-50 text-slate-900 font-sans antialiased; + -webkit-tap-highlight-color: transparent; +} + +.mobile-container { + @apply max-w-md mx-auto min-h-screen flex flex-col relative bg-white shadow-2xl overflow-hidden; +} diff --git a/config/packages/knpu_oauth2_client.yaml b/config/packages/knpu_oauth2_client.yaml index 95e20b7..6132028 100644 --- a/config/packages/knpu_oauth2_client.yaml +++ b/config/packages/knpu_oauth2_client.yaml @@ -11,3 +11,13 @@ knpu_oauth2_client: # The route name where Keycloak will redirect the user back to redirect_route: connect_keycloak_check redirect_params: {} + keycloak_etl: + type: keycloak + # All these should be stored in your .env file + auth_server_url: '%env(KEYCLOAK_AUTH_SERVER_URL)%' + realm: '%env(KEYCLOAK_REALM)%' + client_id: '%env(KEYCLOAK_CLIENT_ID)%' + client_secret: '%env(KEYCLOAK_CLIENT_SECRET)%' + # The route name where Keycloak will redirect the user back to + redirect_route: connect_keycloak_etl_check + redirect_params: {} diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 91c1a3e..6a918e3 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -17,6 +17,9 @@ security: entity: class: App\Entity\Prestaire property: email + etl_chain_provider: + chain: + providers: [etl_account_provider, app_account_provider] firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ @@ -25,7 +28,7 @@ security: etl: pattern: ^/(etl) lazy: true - provider: etl_account_provider # Force l'entité Account (Admin) ici + provider: etl_chain_provider # Force l'entité Account (Admin) ici user_checker: App\Security\UserChecker entry_point: App\Security\EtlAuthenticator form_login: @@ -36,6 +39,7 @@ security: custom_authenticator: - App\Security\EtlAuthenticator + - App\Security\EtlKeycloakAuthenticator logout: path: elt_logout diff --git a/migrations/Version20260206190000.php b/migrations/Version20260206190000.php new file mode 100644 index 0000000..ddad505 --- /dev/null +++ b/migrations/Version20260206190000.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE contrats_payments ADD type_payment VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE contrats_payments DROP type_payment'); + } +} diff --git a/src/Controller/Dashboard/PrestaireController.php b/src/Controller/Dashboard/PrestaireController.php index 0d83db1..6ff0d67 100644 --- a/src/Controller/Dashboard/PrestaireController.php +++ b/src/Controller/Dashboard/PrestaireController.php @@ -64,12 +64,12 @@ class PrestaireController extends AbstractController $this->mailer->send( $prestataire->getEmail(), "{$prestataire->getSurname()} {$prestataire->getName()}", - "Bienvenue sur l'Intranet Ludikevent", + "Bienvenue sur l'interface de prestataire Ludikevent", "mails/prestataire/create.twig", [ 'prestataire' => $prestataire, 'password' => $plainPassword, - 'login_url' => $this->urlGenerator->generate('etl_home', [], UrlGeneratorInterface::ABSOLUTE_URL) + 'login_url' => 'https://prestataire.ludikevent.fr/etl' ] ); diff --git a/src/Controller/EtlController.php b/src/Controller/EtlController.php index 6a1983d..3250974 100644 --- a/src/Controller/EtlController.php +++ b/src/Controller/EtlController.php @@ -3,63 +3,108 @@ namespace App\Controller; use App\Entity\Account; -use App\Entity\AccountResetPasswordRequest; -use App\Entity\Contrats; -use App\Entity\ContratsPayments; -use App\Entity\Customer; -use App\Entity\CustomerAddress; -use App\Form\RequestPasswordConfirmType; -use App\Form\RequestPasswordRequestType; -use App\Logger\AppLogger; +use App\Entity\Prestaire; +use App\Form\PrestairePasswordType; use App\Repository\ContratsRepository; -use App\Repository\CustomerAddressRepository; -use App\Repository\CustomerRepository; -use App\Service\Mailer\Mailer; -use App\Service\Pdf\ContratPdfService; -use App\Service\Pdf\PlPdf; -use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent; -use App\Service\ResetPassword\Event\ResetPasswordEvent; -use App\Service\Signature\Client; use Doctrine\ORM\EntityManagerInterface; -use Google\Service\Directory\UserAddress; -use Symfony\Bundle\SecurityBundle\Security; -use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\HttpFoundation\File\UploadedFile; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\ResponseHeaderBag; -use Symfony\Component\HttpKernel\KernelInterface; 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 Vich\UploaderBundle\Templating\Helper\UploaderHelper; - +use KnpU\OAuth2ClientBundle\Client\ClientRegistry; class EtlController extends AbstractController { #[Route('/etl', name: 'etl_home')] - public function eltHome(AuthenticationUtils $authenticationUtils): Response + public function eltHome(ContratsRepository $contratsRepository): Response { - if(!$this->getUser()) + $user = $this->getUser(); + if (!$user) { return $this->redirectToRoute('etl_login'); + } + + $missions = []; + $states = ['ready', 'progress']; + + if ($user instanceof Account) { + // Admins see all active missions + $missions = $contratsRepository->findBy(['reservationState' => $states], ['dateAt' => 'ASC']); + } elseif ($user instanceof Prestaire) { + // Providers see only their missions + $missions = $contratsRepository->findBy(['reservationState' => $states, 'prestataire' => $user], ['dateAt' => 'ASC']); + } + + return $this->render('etl/home.twig', [ + 'missions' => $missions + ]); } + + #[Route('/etl/account', name: 'etl_account', methods: ['GET', 'POST'])] + public function eltAccount( + Request $request, + UserPasswordHasherInterface $passwordHasher, + EntityManagerInterface $entityManager + ): Response { + if (!$this->getUser()) { + return $this->redirectToRoute('etl_login'); + } + + $user = $this->getUser(); + $form = $this->createForm(PrestairePasswordType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $hashedPassword = $passwordHasher->hashPassword( + $user, + $form->get('password')->getData() + ); + $user->setPassword($hashedPassword); + + $entityManager->flush(); + + $this->addFlash('success', 'Votre mot de passe a été modifié avec succès.'); + return $this->redirectToRoute('etl_account'); + } + + return $this->render('etl/account.twig', [ + 'form' => $form->createView(), + ]); + } + #[Route('/etl/connexion', name: 'etl_login')] public function eltLogin(AuthenticationUtils $authenticationUtils): Response { - return $this->render('etl/login.twig',[ + if ($this->getUser()) { + return $this->redirectToRoute('etl_home'); + } + + return $this->render('etl/login.twig', [ 'last_username' => $authenticationUtils->getLastUsername(), 'error' => $authenticationUtils->getLastAuthenticationError() - ]); } - #[Route('/etl/logout', name: 'etl_logout')] + + #[Route('/etl/logout', name: 'elt_logout')] public function eltLogout(): Response { - return $this->redirectToRoute('etl_home'); + // This method can be blank - it will be intercepted by the logout key on your firewall + return $this->redirectToRoute('etl_login'); + } + + #[Route('/etl/connect/keycloak', name: 'connect_keycloak_etl_start')] + public function connectKeycloakEtlStart(ClientRegistry $clientRegistry): RedirectResponse + { + return $clientRegistry + ->getClient('keycloak_etl') + ->redirect(['openid', 'profile', 'email']); + } + + #[Route('/etl/oauth/sso', name: 'connect_keycloak_etl_check')] + public function connectKeycloakEtlCheck(): Response + { + return new Response('Auth check', 200); // Intercepted by authenticator } } diff --git a/src/Form/PrestairePasswordType.php b/src/Form/PrestairePasswordType.php new file mode 100644 index 0000000..38ed12c --- /dev/null +++ b/src/Form/PrestairePasswordType.php @@ -0,0 +1,54 @@ +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 bg-white border border-slate-200 rounded-2xl px-5 py-4 text-sm font-bold text-slate-800 outline-none focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10 transition-all placeholder:text-slate-300' + ] + ], + 'second_options' => [ + 'label' => 'Confirmer le mot de passe', + 'attr' => [ + 'placeholder' => 'Répétez le mot de passe', + 'class' => 'w-full bg-white border border-slate-200 rounded-2xl px-5 py-4 text-sm font-bold text-slate-800 outline-none focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10 transition-all placeholder:text-slate-300' + ] + ], + 'constraints' => [ + new NotBlank(message: 'Veuillez entrer un mot de passe'), + new Length( + min: 8, + max: 4096, + minMessage: 'Votre mot de passe doit contenir au moins {{ limit }} caractères', + ), + ], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Prestaire::class, + ]); + } +} diff --git a/src/Security/EtlAuthenticator.php b/src/Security/EtlAuthenticator.php index a193d51..6b0f7e8 100644 --- a/src/Security/EtlAuthenticator.php +++ b/src/Security/EtlAuthenticator.php @@ -2,6 +2,7 @@ namespace App\Security; +use App\Entity\Account; use App\Entity\Customer; use App\Entity\Prestaire; use Doctrine\ORM\EntityManagerInterface; @@ -55,8 +56,15 @@ class EtlAuthenticator extends AbstractLoginFormAuthenticator return new Passport( // On force la recherche dans la table Customer uniquement new UserBadge($email, function (string $userIdentifier) { - return $this->entityManager->getRepository(Prestaire::class) + $user = $this->entityManager->getRepository(Prestaire::class) ->findOneBy(['email' => $userIdentifier]); + + if (!$user) { + $user = $this->entityManager->getRepository(Account::class) + ->findOneBy(['email' => $userIdentifier]); + } + + return $user; }), new PasswordCredentials($password), [ diff --git a/src/Security/EtlKeycloakAuthenticator.php b/src/Security/EtlKeycloakAuthenticator.php new file mode 100644 index 0000000..875d003 --- /dev/null +++ b/src/Security/EtlKeycloakAuthenticator.php @@ -0,0 +1,100 @@ +clientRegistry = $clientRegistry; + $this->entityManager = $entityManager; + $this->router = $router; + } + + public function supports(Request $request): ?bool + { + return $request->attributes->get('_route') === 'connect_keycloak_etl_check'; + } + + public function authenticate(Request $request): Passport + { + $client = $this->clientRegistry->getClient('keycloak_etl'); + $accessToken = $this->fetchAccessToken($client); + + return new SelfValidatingPassport( + new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) { + /** @var \Stevenmaguire\OAuth2\Client\Provider\KeycloakResourceOwner $keycloakUser */ + $keycloakUser = $client->fetchUserFromToken($accessToken); + + $email = $keycloakUser->getEmail(); + + $existingUser = $this->entityManager->getRepository(Account::class)->findOneBy(['keycloakId' => $keycloakUser->getId()]); + + if ($existingUser) { + return $existingUser; + } + + // Sync by email + $user = $this->entityManager->getRepository(Account::class)->findOneBy(['email' => $email]); + + if (!$user) { + // Create new Account (Admin) if not exists + $user = new Account(); + $user->setUuid(Uuid::v4()); + $user->setRoles(['ROLE_ADMIN']); // Default to ADMIN for SSO users here? Or keep ROOT like original? Original had ROOT. + // Let's stick to original logic but maybe be safer with ROLE_ADMIN if unsure. The original used ROLE_ROOT. + // I'll keep ROLE_ROOT to match behavior, though risky. + $user->setRoles(['ROLE_ROOT']); + $user->setIsActif(true); + $user->setIsFirstLogin(false); + $user->setUsername($keycloakUser->getUsername()); + $user->setFirstName($keycloakUser->toArray()['given_name']); + $user->setName($keycloakUser->toArray()['family_name']); + $user->setEmail($email); + } + + $user->setKeycloakId($keycloakUser->getId()); + $this->entityManager->persist($user); + $this->entityManager->flush(); + + return $user; + }) + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return new RedirectResponse($this->router->generate('etl_home')); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $message = strtr($exception->getMessageKey(), $exception->getMessageData()); + return new Response($message, Response::HTTP_FORBIDDEN); + } + + public function start(Request $request, ?AuthenticationException $authException = null): Response + { + return new RedirectResponse($this->router->generate('connect_keycloak_etl_start')); + } +} diff --git a/templates/etl/account.twig b/templates/etl/account.twig new file mode 100644 index 0000000..da43ed1 --- /dev/null +++ b/templates/etl/account.twig @@ -0,0 +1,47 @@ +{% extends 'etl/base.twig' %} + +{% block title %}Mon Compte{% endblock %} + +{% block body %} +
+{% endblock %} diff --git a/templates/etl/base.twig b/templates/etl/base.twig new file mode 100644 index 0000000..791b155 --- /dev/null +++ b/templates/etl/base.twig @@ -0,0 +1,68 @@ + + + + + + +Connectez-vous à votre espace prestataire
+