Valider l'email de l'utilisateur
Nous allons générer un code à six chiffres que nous enverrons par mail avec une date d'expiration. Il faudra également pouvoir renvoyer un nouveau code si celui-ci est expiré.
Dans notre entité User
nous rajoutons ces deux attributs ainsi que ces getters et setters :
/**
* @ORM\Column(type="integer")
*/
private $activationCode;
/**
* @ORM\Column(type="datetime")
*/
private $activationCodeExpiresAt;
/**
* @Groups({"user:read"})
* @ORM\Column(type="boolean")
*/
private $isEnabled;
public function __construct()
{
$this->isEnabled = false;
}
// ...
public function getActivationCode(): ?int
{
return $this->activationCode;
}
public function setActivationCode(int $activationCode): self
{
$this->activationCode = $activationCode;
return $this;
}
public function getActivationCodeExpiresAt(): ?\DateTimeInterface
{
return $this->activationCodeExpiresAt;
}
public function setActivationCodeExpiresAt(\DateTimeInterface $activationCodeExpiresAt): self
{
$this->activationCodeExpiresAt = $activationCodeExpiresAt;
return $this;
}
public function getIsEnabled(): ?bool
{
return $this->isEnabled;
}
public function setIsEnabled(bool $isEnabled): self
{
$this->isEnabled = $isEnabled;
return $this;
}
Nous mettons à jour notre base de donnée avec php bin/console doctrine:shema:update --force
. Faites attention à bien avoir mis isEnabled
à false
dans la méthode __construct
Afin de pouvoir envoyer des emails avec notre application nous allons utiliser le bundle mailer.
S'il n'est pas sur votre application vous pouvez l'ajouter :
composer require symfony/mailer
Si vous utilisez docker vous pouvez créer un fichier docker-compose.override.yml
dans lequel vous pouvez ajouter le container maildev
qui nous permettra de capturer les mails sortant
version: '3.8'
services:
maildev:
image: maildev/maildev
command: bin/maildev --web 80 --smtp 25 --hide-extensions STARTTLS
ports:
- "8081:80"
Après avoir relancer vos containers si vous vous rendez sur l'adresse http://127.0.0.1:8081
vous devriez avoir le webmail
Dans le fichier .env
pour configurer symfony/mailer
il faut indiquer MAILER_DSN=smtp://maildev:25
(si vous utilisez le container et votre service doit s'appeler "maildev")
Sinon si vous n'utilisez pas docker renseignez : MAILER_DSN=smtp://localhost
Générer le code et sa date d'expiration
Nous allons créer une classe avec une méthode statique (pour nous éviter d'instancier la classe)
Créez la classe et le dossier comme suit src/SecurityActivationCodeService.php
Voici la méthode que je vous propose
<?php
namespace App\Security;
class ActivationCodeService
{
const VALIDATION_HOURS = 2;
public static function generateNewCode()
{
return
[
'code' => random_int(100000, 999999),
'expirationDate' => new \DateTime('+ ' . self::VALIDATION_HOURS . 'hour')
];
}
}
pour l'utiliser nous n'aurons plus qu'à faire ActivationCodeService::generateNewCode()
et nous aurons en retour un array
array:2 [
"code" => 744763
"expirationDate" => DateTime @1692535362 {#1749
date: 2023-08-20 12:42:42.708807 UTC (+00:00)
}
]
Il faut enregistrer ses informations et les envoyer par mail.
Dans notre UserDataPersister
dans notre méthode persist nous rajoutons le persist du code ainsi que l'envoie du mail. Ainsi, dès qu'un utilisateur sera créé, il recevra un code généré par mail.
use App\Entity\User;
use App\Security\ActivationCodeService;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Twig\Environment;
public function persist($data, array $context = [])
{
if ($data->getId() === null) {
if ($data->getPlainPassword()) {
$data->setPassword(
$this->userPasswordEncoder->encodePassword($data, $data->getPlainPassword())
);
$data->eraseCredentials();
}
$activationCode = ActivationCodeService::generateNewCode();
$data->setActivationCode($activationCode['code']);
$data->setActivationCodeExpiresAt($activationCode['expirationDate']);
$email = (new Email())
->from('hello@example.com')
->to($data->getEmail())
->subject('Activez votre mail')
->html(
$this->twig->render('emails/registration.html.twig', [
'activationCode' => $activationCode
])
)
;
$this->mailer->send($email);
}
return $this->decorated->persist($data, $context);
}
Il faut également créer le template templates/email/registration.html.twig
<h3>Bienvenue</h3>
<div>
Votre code d'activation est <strong>{{ activationCode.code }}</strong>
</div>
<div>
Ce code est valable jusqu'au <strong>{{ activationCode.expirationDate|date('Y-m-d H:i:s') }}</strong>
</div>
Maintenant quand un nouvel utilisateur s'inscrit vous pouvez voir le mail qu'il reçoit sur http://127.0.0.1:8081
Valider le code
Maintenant nous avons besoin d'une route qui va nous permettre de vérifier si ce code est valide et non expiré.
Nous allons créer un DTO ActivationCodeInput
dans lequel nous mettrons les attributs code
et email
. Ensuite on créé ActivationCodeDataPersister
pour récupérer l'objet ActivationCodeDataTransformer
afin de pouvoir faire notre logique et vérifier si le code est valide et non expiré.
<?php
// src/DTO/ActivationCodeInput.php
namespace App\DTO;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\Groups;
class ActivationCodeInput
{
/**
* @Assert\NotBlank()
* @Groups({"user:write"})
*/
public $code;
/**
* @Assert\Email()
* @Assert\Length(max="180")
* @Groups({"user:write"})
*/
public $email;
}
On créé la route dans l'entité User et on indique à input
notre DTO
/**
* @ORM\Entity(repositoryClass=UserRepository::class)
* @ApiResource(
* normalizationContext={"groups"={"user:read"}},
* denormalizationContext={"groups"={"user:write"}},
* collectionOperations={
* "get",
* "post",
* "activationCode"={
* "method"="POST",
* "path"="activation-code",
* "input": ActivationCodeInput::class,
* "denormalization_context"={"groups"={"activation:write"}}
* }
* }
* )
*/
class User implements UserInterface
Notre DataTransformer nous permet de récupérer notre DTO. Donc $data
dans la méthode transform
est notre objet ActivationCodeInput
<?php
// src/DataTransformer/ActivationCodeDataTransformer.php
namespace App\DataTransformer;
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use App\Entity\User;
final class ActivationCodeDataTransformer implements DataTransformerInterface
{
/**
* {@inheritdoc}
*/
public function transform($data, string $to, array $context = [])
{
return $data;
}
/**
* {@inheritdoc}
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
// in the case of an input, the value given here is an array (the JSON decoded).
// if it's a user we transformed the data already
if ($data instanceof User) {
return false;
}
return (User::class === $to) && null !== ($context['input']['class'] ?? null) && (array_key_exists('collection_operation_name', $context) && $context['collection_operation_name'] === 'activationCode');
}
}
Notre entité User
Notre DTO input ActivationCodeInput
Notre DataTransformer ActivationCodeDataTransformer
Vérifier si le code est valide et s'il n'est pas expiré
Nous allons créer une classe, et un validateur qui vérifiera le code et sa date. Ce validateur pourra nous indiquer si :
- l'utilisateur existe (en cherchant l'utilisateur avec l'email)
- vérifier que le code est valide
- vérifier que la date est valide
<?php
// src/DTO/ActivationCodeValidator.php
namespace App\DTO;
use App\Validator\Constraint\IsValidActivationCode;
/**
* @IsValidActivationCode
*/
class ActivationCodeValidator
{
public $code;
public $email;
}
Nous allons créer le validateur IsValidActivationCode
<?php
// src/Validator/Constraint/IsValidActivationCode.php
namespace App\Validator\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"CLASS"})
*/
class IsValidActivationCode extends Constraint
{
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
<?php
// src/Validator/Constraint/IsValidActivationCodeValidator.php
namespace App\Validator\Constraint;
use App\DTO\ActivationCodeValidator;
use App\Entity\CheeseListing;
use App\Repository\UserRepository;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class IsValidActivationCodeValidator extends ConstraintValidator
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function validate($value, Constraint $constraint)
{
if (!$value instanceof ActivationCodeValidator) {
throw new \Exception('Only ActivationCodeValidator is supported');
}
$user = $this->userRepository->findOneBy(['email' => $value->email]);
if (null === $user) {
$this->context->buildViolation("Cet utilisateur n'existe pas")
->addViolation()
;
return;
}
if ($user->getActivationCodeExpiresAt() < new \DateTime()) {
$this->context->buildViolation("Ce code est expiré")
->addViolation()
;
return;
}
if ($value->code !== $user->getActivationCode()) {
$this->context->buildViolation("Ce code n'est pas correct")
->addViolation()
;
return;
}
}
}
Notre IsValidActivationCodeValidator
nous permet de vérifier si l'email existe en base, si le code est expiré et s'il est valide. Il y a un return dans la condition car nous n'avons pas besoin de continuer les vérifications quand il rencontre une violation.
Nous créons dans notre ActivationCodeDataPersister
et nous faisons la vérifications des données sur notre nouvelle classe ActivationCodeValidator, si il n'y a pas d'erreur, on peut continuer et on valide notre User avec setIsEnabled(true)
<?php
// src/DataPersister/ActivationCodeDataPersister.php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\DTO\ActivationCodeInput;
use App\DTO\ActivationCodeValidator;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class ActivationCodeDataPersister implements ContextAwareDataPersisterInterface
{
private $validator;
private $userRepository;
private $entityManager;
public function __construct(ValidatorInterface $validator, UserRepository $userRepository, EntityManagerInterface $entityManager) {
$this->validator = $validator;
$this->userRepository = $userRepository;
$this->entityManager = $entityManager;
}
public function supports($data, array $context = []): bool
{
return $data instanceof ActivationCodeInput;
}
public function persist($data, array $context = [])
{
$activationCodeValidator = new ActivationCodeValidator();
$activationCodeValidator->code = $data->code;
$activationCodeValidator->email = $data->email;
$errors = $this->validator->validate($activationCodeValidator);
if ($errors->count() > 0) {
return $errors;
}
// Once we know everything is valid, we can activate our User
$user = $this->userRepository->findOneBy(['email' => $data->email]);
$user->setIsEnabled(true);
// We expire the code by setting the date to now
$user->setActivationCodeExpiresAt(new \DateTime());
$this->entityManager->persist($user);
$this->entityManager->flush();
return new JsonResponse(['message' => 'Your address is enabled']);
}
public function remove($data, array $context = [])
{
}
}
Notre condition dans supportsTransformation
vérifie maintenant si on est sur la route activationCode
pour être sûr de rentrer dans ce Transformer uniquement avec cette route
Commentaires
Connectez-vous pour laisser un commentaire