diff --git a/.env b/.env index 63c72abd7..f640111a5 100644 --- a/.env +++ b/.env @@ -18,6 +18,7 @@ APP_ENV=dev APP_SERVER_TIMEZONE=UTC APP_CLIENT_TIMEZONE=Europe/Paris APP_SECRET=abc +BASE_URL=https://dialog.beta.gouv.fr APP_EUDONET_PARIS_BASE_URL=https://eudonet-partage.apps.paris.fr APP_BAC_IDF_DECREES_FILE=data/bac_idf/decrees.json APP_BAC_IDF_CITIES_FILE=data/bac_idf/cities.csv diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 69ea6e298..51e2d9b67 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -3,6 +3,7 @@ twig: form_themes: ['common/form/dsfr_theme.html.twig'] globals: dialogOrgId: '%env(DIALOG_ORG_ID)%' + baseUrl: '%env(BASE_URL)%' when@test: twig: strict_variables: true diff --git a/docker-compose.yml b/docker-compose.yml index 0031f6206..f633f05b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,8 @@ services: volumes: - ./:/var/www/dialog - ./docker/php/supervisor:/etc/supervisor - - ./var/log/supervisor:/var/log/supervisor + environment: + DATABASE_URL: ${DATABASE_URL} depends_on: - database - redis @@ -95,3 +96,4 @@ services: - REDIS_HOSTS=local:redis:6379 depends_on: - redis + - database diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 21f656272..f7d81cd96 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -55,6 +55,7 @@ RUN apt-get install -y wget && \ # Install Supervisor +RUN mkdir -p /var/log/supervisor && chown www-data:www-data /var/log/supervisor RUN apt-get update && apt-get install -y supervisor RUN mkdir -p /var/log/supervisor COPY supervisor/supervisord.conf /etc/supervisor/supervisord.conf diff --git a/docker/php/supervisor/conf.d/messenger-worker.conf b/docker/php/supervisor/conf.d/messenger-worker.conf index 1d59cbd5c..bbefa0eb5 100644 --- a/docker/php/supervisor/conf.d/messenger-worker.conf +++ b/docker/php/supervisor/conf.d/messenger-worker.conf @@ -5,3 +5,5 @@ autostart=true autorestart=true process_name=%(program_name)s_%(process_num)02d user=www-data +stdout_logfile=/var/log/supervisor/messenger-worker_%(process_num)02d.log +stderr_logfile=/var/log/supervisor/messenger-worker_%(process_num)02d.error.log diff --git a/docker/php/supervisor/supervisord.conf b/docker/php/supervisor/supervisord.conf index bb5a27d7a..8ba68c1ff 100644 --- a/docker/php/supervisor/supervisord.conf +++ b/docker/php/supervisor/supervisord.conf @@ -15,4 +15,4 @@ supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface serverurl=unix:///var/run/supervisor.sock [include] -files = /etc/suggpervisor/conf.d/*.conf +files = /etc/supervisor/conf.d/*.conf diff --git a/src/Application/MailerInterface.php b/src/Application/MailerInterface.php new file mode 100644 index 000000000..fa97cff21 --- /dev/null +++ b/src/Application/MailerInterface.php @@ -0,0 +1,12 @@ +email)); + + try { + $token = $this->commandBus->handle( + new CreateTokenCommand( + $email, + TokenTypeEnum::FORGOT_PASSWORD->value, + ), + ); + + $this->mailer->send( + new Mail( + address: $email, + subject: 'forgot_password.subjet', + template: 'email/user/forgot-password.html.twig', + payload: [ + 'token' => $token, + ], + ), + ); + } catch (UserNotFoundException) { + // Do nothing. + } + } +} diff --git a/src/Application/User/Command/ResetPasswordCommand.php b/src/Application/User/Command/ResetPasswordCommand.php new file mode 100644 index 000000000..6847b500d --- /dev/null +++ b/src/Application/User/Command/ResetPasswordCommand.php @@ -0,0 +1,17 @@ +tokenRepository->findOneByTokenAndType( + $command->token, + TokenTypeEnum::FORGOT_PASSWORD->value, + ); + + if (!$token instanceof Token) { + throw new TokenNotFoundException(); + } + + if ($this->isTokenExpired->isSatisfiedBy($token)) { + throw new TokenExpiredException(); + } + + $password = $this->passwordHasher->hash($command->password); + $token->getUser()->getPasswordUser()->setPassword($password); + + $this->tokenRepository->remove($token); + } +} diff --git a/src/Domain/Mail.php b/src/Domain/Mail.php new file mode 100644 index 000000000..2bf48bed9 --- /dev/null +++ b/src/Domain/Mail.php @@ -0,0 +1,16 @@ +dateUtils->getNow() > $token->getExpirationDate(); + } +} diff --git a/src/Infrastructure/Adapter/Mailer.php b/src/Infrastructure/Adapter/Mailer.php new file mode 100644 index 000000000..bc24063ca --- /dev/null +++ b/src/Infrastructure/Adapter/Mailer.php @@ -0,0 +1,32 @@ +mailer->send( + (new TemplatedEmail()) + ->to(new Address($mail->address)) + ->subject($this->translator->trans($mail->subject, [], 'emails')) + ->htmlTemplate($mail->template) + ->context($mail->payload), + ); + } +} diff --git a/src/Infrastructure/Controller/Security/ForgotPasswordController.php b/src/Infrastructure/Controller/Security/ForgotPasswordController.php index c1d56cf3e..ed51a8d67 100644 --- a/src/Infrastructure/Controller/Security/ForgotPasswordController.php +++ b/src/Infrastructure/Controller/Security/ForgotPasswordController.php @@ -4,21 +4,56 @@ namespace App\Infrastructure\Controller\Security; +use App\Application\CommandBusInterface; +use App\Application\User\Command\Mail\SendForgotPasswordMailCommand; +use App\Infrastructure\Form\User\ForgotPasswordFormType; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; -final class ForgotPasswordController +final readonly class ForgotPasswordController { public function __construct( private \Twig\Environment $twig, + private FormFactoryInterface $formFactory, + private CommandBusInterface $commandBus, + private UrlGeneratorInterface $urlGenerator, + private TranslatorInterface $translator, ) { } - #[Route('/mot-de-passe-oublie', name: 'app_forgot_password', methods: ['GET'])] - public function __invoke(): Response + #[Route('/forgot-password', name: 'app_forgot_password', methods: ['GET', 'POST'])] + public function __invoke(Request $request): Response { + $command = new SendForgotPasswordMailCommand(); + $form = $this->formFactory->create(ForgotPasswordFormType::class, $command); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->commandBus->dispatchAsync($command); + + /** @var FlashBagAwareSessionInterface */ + $session = $request->getSession(); + $session->getFlashBag()->add('success', $this->translator->trans('forgot_password.succeeded')); + + return new RedirectResponse($this->urlGenerator->generate('app_forgot_password')); + } + return new Response( - $this->twig->render('forgot-password.html.twig'), + content: $this->twig->render( + name: 'forgot-password.html.twig', + context: [ + 'form' => $form->createView(), + ], + ), + status: ($form->isSubmitted() && !$form->isValid()) + ? Response::HTTP_UNPROCESSABLE_ENTITY + : Response::HTTP_OK, ); } } diff --git a/src/Infrastructure/Controller/Security/ResetPasswordController.php b/src/Infrastructure/Controller/Security/ResetPasswordController.php new file mode 100644 index 000000000..934083bdf --- /dev/null +++ b/src/Infrastructure/Controller/Security/ResetPasswordController.php @@ -0,0 +1,67 @@ +getSession(); + + $command = new ResetPasswordCommand($token); + $form = $this->formFactory->create(ResetPasswordFormType::class, $command); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + try { + $this->commandBus->handle($command); + $session->getFlashBag()->add('success', $this->translator->trans('reset_password.succeeded')); + + return new RedirectResponse($this->urlGenerator->generate('app_login')); + } catch (TokenNotFoundException|TokenExpiredException) { + $session->getFlashBag()->add('error', $this->translator->trans('reset_password.token.error')); + + return new RedirectResponse($this->urlGenerator->generate('app_forgot_password')); + } + } + + return new Response( + content: $this->twig->render( + name: 'reset-password.html.twig', + context: [ + 'form' => $form->createView(), + ], + ), + status: ($form->isSubmitted() && !$form->isValid()) + ? Response::HTTP_UNPROCESSABLE_ENTITY + : Response::HTTP_OK, + ); + } +} diff --git a/src/Infrastructure/Form/User/ForgotPasswordFormType.php b/src/Infrastructure/Form/User/ForgotPasswordFormType.php new file mode 100644 index 000000000..f7825e409 --- /dev/null +++ b/src/Infrastructure/Form/User/ForgotPasswordFormType.php @@ -0,0 +1,28 @@ +add('email', EmailType::class, [ + 'label' => 'register.email', + ]) + ->add('save', SubmitType::class, + options: [ + 'label' => 'common.submit', + 'attr' => ['class' => 'fr-btn'], + ], + ) + ; + } +} diff --git a/src/Infrastructure/Form/User/ResetPasswordFormType.php b/src/Infrastructure/Form/User/ResetPasswordFormType.php new file mode 100644 index 000000000..f33b506b8 --- /dev/null +++ b/src/Infrastructure/Form/User/ResetPasswordFormType.php @@ -0,0 +1,36 @@ +add('password', RepeatedType::class, [ + 'type' => PasswordType::class, + 'first_options' => [ + 'label' => 'reset_password.password', + 'help' => 'register.password.help', + ], + 'second_options' => [ + 'label' => 'reset_password.repeated_password', + ], + ]) + ->add('save', SubmitType::class, + options: [ + 'label' => 'reset_password.submit', + 'attr' => ['class' => 'fr-btn'], + ], + ) + ; + } +} diff --git a/templates/email/base.html.twig b/templates/email/base.html.twig new file mode 100644 index 000000000..0b8686af3 --- /dev/null +++ b/templates/email/base.html.twig @@ -0,0 +1,20 @@ + + +
+ + + + +
+ {% block body %}{% endblock %}
+
+ {{ 'common.best_regards'|trans({}, 'emails') }} |
+
{{ 'forgot_password.title'|trans }}
++ + {{ 'forgot_password.reset'|trans }} + +
+{% endblock %} diff --git a/templates/forgot-password.html.twig b/templates/forgot-password.html.twig index b5a4e8cac..a3180a93e 100644 --- a/templates/forgot-password.html.twig +++ b/templates/forgot-password.html.twig @@ -12,9 +12,13 @@{{ 'forgot_password.instructions'|trans }}
-- {{ 'forgot_password.go_back'|trans }} +
{{ 'forgot_password.description'|trans }}
+ {{ form_start(form) }} + {{ form_row(form.email, {group_class: 'fr-input-group', attr: {class: 'fr-input'} }) }} + {{ form_row(form.save) }} + {{ form_end(form) }} +{{ 'register.meta.subtitle'|trans }}
+