Skip to content

Commit

Permalink
Ajout de la fonctionnalité mot de passe oublié
Browse files Browse the repository at this point in the history
  • Loading branch information
mmarchois committed Jan 23, 2025
1 parent b55c87a commit 0c0c40d
Show file tree
Hide file tree
Showing 29 changed files with 855 additions and 16 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions config/packages/twig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -95,3 +96,4 @@ services:
- REDIS_HOSTS=local:redis:6379
depends_on:
- redis
- database
1 change: 1 addition & 0 deletions docker/php/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docker/php/supervisor/conf.d/messenger-worker.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion docker/php/supervisor/supervisord.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 12 additions & 0 deletions src/Application/MailerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace App\Application;

use App\Domain\Mail;

interface MailerInterface
{
public function send(Mail $mail): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Command\Mail;

use App\Application\AsyncCommandInterface;

final class SendForgotPasswordMailCommand implements AsyncCommandInterface
{
public ?string $email;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Command\Mail;

use App\Application\CommandBusInterface;
use App\Application\MailerInterface;
use App\Application\User\Command\CreateTokenCommand;
use App\Domain\Mail;
use App\Domain\User\Enum\TokenTypeEnum;
use App\Domain\User\Exception\UserNotFoundException;

final readonly class SendForgotPasswordMailCommandHandler
{
public function __construct(
private CommandBusInterface $commandBus,
private MailerInterface $mailer,
) {
}

public function __invoke(SendForgotPasswordMailCommand $command): void
{
$email = trim(strtolower($command->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.
}
}
}
17 changes: 17 additions & 0 deletions src/Application/User/Command/ResetPasswordCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Command;

use App\Application\CommandInterface;

final class ResetPasswordCommand implements CommandInterface
{
public ?string $password = '';

public function __construct(
public readonly string $token,
) {
}
}
44 changes: 44 additions & 0 deletions src/Application/User/Command/ResetPasswordCommandHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Command;

use App\Application\PasswordHasherInterface;
use App\Domain\User\Enum\TokenTypeEnum;
use App\Domain\User\Exception\TokenExpiredException;
use App\Domain\User\Exception\TokenNotFoundException;
use App\Domain\User\Repository\TokenRepositoryInterface;
use App\Domain\User\Specification\IsTokenExpired;
use App\Domain\User\Token;

final readonly class ResetPasswordCommandHandler
{
public function __construct(
private TokenRepositoryInterface $tokenRepository,
private IsTokenExpired $isTokenExpired,
private PasswordHasherInterface $passwordHasher,
) {
}

public function __invoke(ResetPasswordCommand $command): void
{
$token = $this->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);
}
}
16 changes: 16 additions & 0 deletions src/Domain/Mail.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace App\Domain;

final readonly class Mail
{
public function __construct(
public string $address,
public string $subject,
public string $template,
public array $payload = [],
) {
}
}
9 changes: 9 additions & 0 deletions src/Domain/User/Exception/TokenExpiredException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace App\Domain\User\Exception;

final class TokenExpiredException extends \Exception
{
}
9 changes: 9 additions & 0 deletions src/Domain/User/Exception/TokenNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace App\Domain\User\Exception;

final class TokenNotFoundException extends \Exception
{
}
21 changes: 21 additions & 0 deletions src/Domain/User/Specification/IsTokenExpired.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Domain\User\Specification;

use App\Application\DateUtilsInterface;
use App\Domain\User\Token;

final class IsTokenExpired
{
public function __construct(
private readonly DateUtilsInterface $dateUtils,
) {
}

public function isSatisfiedBy(Token $token): bool
{
return $this->dateUtils->getNow() > $token->getExpirationDate();
}
}
32 changes: 32 additions & 0 deletions src/Infrastructure/Adapter/Mailer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace App\Infrastructure\Adapter;

use App\Application\MailerInterface;
use App\Domain\Mail;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\MailerInterface as SymfonyMailer;
use Symfony\Component\Mime\Address;
use Symfony\Contracts\Translation\TranslatorInterface;

final readonly class Mailer implements MailerInterface
{
public function __construct(
private TranslatorInterface $translator,
private SymfonyMailer $mailer,
) {
}

public function send(Mail $mail): void
{
$this->mailer->send(
(new TemplatedEmail())
->to(new Address($mail->address))
->subject($this->translator->trans($mail->subject, [], 'emails'))
->htmlTemplate($mail->template)
->context($mail->payload),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
}
Loading

0 comments on commit 0c0c40d

Please sign in to comment.