Drupal 10 : Mise en Place d’une API d’Activation de Compte et Réémission de Lien

Introduction

Dans le cadre du développement d’une application avec Drupal 10, il est essentiel de fournir aux utilisateurs un processus sécurisé et fluide pour activer leur compte après l’inscription. Pour répondre à ce besoin, nous avons mis en place un système qui génère un lien d’activation envoyé par email, permettant aux utilisateurs d’activer leur compte via une API dédiée. De plus, afin d’assurer une expérience optimale, nous avons également développé une API de réémission qui permet aux utilisateurs de demander un nouveau lien d’activation en cas de perte ou d’expiration. Ce document présente les différentes étapes pour mettre en place ces mécanismes, en partant des prérequis jusqu’aux tests de validation.

Prérequis

Avant de procéder à la configuration, veuillez vous assurer de satisfaire aux conditions suivantes :

Use Case

Dans un environnement Drupal traditionnel, lorsque qu’un utilisateur s’inscrit, il reçoit automatiquement un email de confirmation contenant un lien d’activation. Ce lien redirige l’utilisateur vers le frontend de Drupal, où son compte est immédiatement activé. Ce mécanisme fonctionne parfaitement, car il s’appuie sur les capacités internes de Drupal pour gérer les activations de compte. Cependant, lorsque nous déployons Drupal en mode headless ou découplé, souvent associé à un frontend de type Single Page Application (SPA) tel qu’Angular ou Vue.js, ce mécanisme d’activation standard devient obsolète. En effet, Drupal ne fournit pas par défaut d’endpoint REST pour activer les comptes utilisateurs, ce qui complique la gestion de l’activation dans ce contexte. Pour remédier à cela, nous allons développer un module personnalisé, custom_auth, qui modifie le lien d’activation dans l’email pour rediriger vers la SPA. Ce module crée également une API d’activation permettant de valider le compte utilisateur et une API de réémission du lien en cas de perte ou d’expiration.

Modification du Lien d’Activation dans l’Email

Nous allons implémenter, dans notre module personnalisé custom_auth, un service appelé ActivationLinkModifier, chargé de modifier le corps de l’email en remplaçant le lien d’activation standard de Drupal par un lien qui redirige vers le frontend de l’application. L’URL du frontend est stockée dans le fichier settings.php, ce qui permet une gestion centralisée de la configuration. Le code PHP de ce service utilise une expression régulière pour extraire le lien d’activation original, puis il construit un nouveau lien qui inclut le token et redirige vers notre frontend.

Ajoutez la ligne suivante dans votre fichier settings.php :

$settings['custom_auth.settings']['frontend_url'] = 'https://your-frontend-url.com';
<?php

namespace Drupal\custom_auth\Service;

use Drupal\Core\Config\ConfigFactoryInterface;

/**
 * Class ActivationLinkModifier.
 *
 * Service to modify the activation link in user registration emails.
 */
class ActivationLinkModifier {

  /**
   * The configuration factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * Constructor for ActivationLinkModifier.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The configuration factory.
   */
  public function __construct(ConfigFactoryInterface $config_factory) {
    $this->configFactory = $config_factory;
  }

  /**
   * Modifies the activation link in the email body.
   *
   * @param string $body
   *   The email body to modify.
   *
   * @return string
   *   The modified email body.
   */
  public function modifyActivationLink($body) {
    // Get the frontend URL from the configuration.
    $frontendUrl = $this->configFactory->get('custom_auth.settings')->get('frontend_url');

    // Extract the original activation link.
    if (preg_match('/http[^\s]+user\/registrationpassword\/[0-9]+\/[^\s]+/', $body, $matches)) {
      $original_link = $matches[0];

      // Create a custom activation link pointing to the frontend.
      $custom_link = $frontendUrl . '/activate?token=' . urlencode($original_link);

      // Replace the original link with the custom link in the email body.
      $body = str_replace($original_link, $custom_link, $body);
    }

    return $body;
  }
}

Ce service utilise la méthode modifyActivationLink pour traiter le contenu de l’email. Dans cette méthode, une expression régulière est appliquée à l’aide de preg_match, qui recherche le lien d’activation standard dans le texte de l’email. Si un lien est trouvé, il est stocké dans la variable $original_link. Ensuite, une nouvelle URL d’activation est générée en concaténant l’URL du frontend, récupérée depuis settings.php, avec le token d’activation, obtenu grâce à urlencode, qui garantit que tous les caractères spéciaux de l’URL sont correctement encodés pour éviter toute confusion lors de la redirection. Finalement, la méthode utilise str_replace pour remplacer l’ancien lien par le nouveau dans le corps de l’email, permettant ainsi aux utilisateurs de cliquer sur un lien qui les redirige vers l’interface de l’application.

Le service doit être déclaré dans le fichier custom_auth.services.yml afin que Drupal puisse le reconnaître et l’injecter dans d’autres composants de l’application.

services:
  custom_auth.activation_link_modifier:
    class: Drupal\custom_auth\Service\ActivationLinkModifier
    arguments: ['@config.factory']

Nous utilisons également le hook hook_mail_alter() dans le fichier custom_auth.module pour intercepter et modifier le contenu de l’email avant qu’il ne soit envoyé :

/**
 * Implements hook_mail_alter().
 */
function custom_auth_mail_alter(&$message) {
  if ($message['id'] == 'user_register_no_approval_required' || $message['id'] == 'user_register_pending_approval') {
    $body = is_array($message['body']) ? implode("\n", $message['body']) : $message['body'];
    $activation_link_modifier = \Drupal::service('custom_auth.activation_link_modifier');
    $modified_body = $activation_link_modifier->modifyActivationLink($body);
    $message['body'] = [$modified_body];
  }
}

Dans ce code, nous interceptons le message si son ID correspond à un email d’inscription. Ensuite, nous appliquons notre service ActivationLinkModifier pour mettre à jour le lien d’activation. Ce processus garantit que chaque utilisateur reçoit un lien qui le dirige vers l’interface appropriée, où il pourra compléter son inscription.

Création d’une API d’Activation

Pour offrir plus de flexibilité et permettre à l’application frontend de gérer l’activation des comptes utilisateurs, nous allons créer une API d’activation. Cette API, accessible via une route spécifique, permettra de valider les tokens d’activation envoyés aux utilisateurs par email et d’activer les comptes directement depuis l’application frontend. Pour implémenter cette API dans notre module personnalisé custom_auth, nous commençons par définir une nouvelle route dans le fichier custom_auth.routing.yml.

custom_auth.account_activate:
  path: '/api/activate-account'
  defaults:
    _controller: '\Drupal\custom_auth\Controller\AccountActivationController::activate'
    _title: 'Activate Account'
  methods: [GET]
  requirements:
    _permission: 'access content'

Cette route est accessible via un appel GET et attend un paramètre token qui sera utilisé pour identifier l’utilisateur et activer son compte.

Pour garantir la sécurité de notre API d’activation, nous devons définir une permission dans notre fichier custom_auth.permissions.yml. Ce fichier est essentiel car il permet de contrôler l’accès à notre API et d’assurer que seuls les utilisateurs autorisés peuvent activer des comptes. Voici le contenu du fichier :

access activate account api:
  title: 'Access API to activate account'
  description: 'Allows users to access the API to activate their account'
  restrict access: TRUE

Maintenant que nous avons défini la route et les permissions, nous pouvons aborder le contrôleur qui gérera l’activation des comptes. Voici le code du contrôleur AccountActivationController :

<?php

namespace Drupal\custom_auth\Controller;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Drupal\user\Entity\User;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * Controller to handle account activation via API.
 */
class AccountActivationController extends ControllerBase {

  /**
   * Activates the user account based on the token.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   A JSON response indicating the status of the activation.
   */
  public function activate(Request $request): JsonResponse {
    // Get the token from the request.
    $token = $this->getTokenFromRequest($request);

    // Extract the UID and timestamp from the token.
    list($uid, $timestamp) = $this->parseToken($token);

    // Load the user by UID.
    $user = $this->loadUser($uid);

    // Check if the user is already active.
    $this->checkUserStatus($user);

    // Verify the token timestamp is valid.
    $this->validateTokenTimestamp($timestamp);

    // Activate the user account.
    $this->activateUser($user);

    return new JsonResponse(['message' => 'Account activated successfully.']);
  }

  /**
   * Gets the token expiration time from Drupal's default configuration.
   *
   * @return int
   *   The token expiration time in seconds.
   */
  private function getTokenExpirationTime(): int {
    $expiration_time = \Drupal::config('user.settings')->get('password_reset_timeout');
    return $expiration_time ?? 86400;
  }

  private function getTokenFromRequest(Request $request): string {
    $token = $request->query->get('token');
    if (!$token) {
      throw new AccessDeniedHttpException('No token provided.');
    }
    return $token;
  }

  private function parseToken(string $token): array {
    if (preg_match('/user\/registrationpassword\/(\d+)\/(\d+)/', urldecode($token), $matches)) {
      return [$matches[1], $matches[2]];
    }
    throw new AccessDeniedHttpException('Invalid token.');
  }

  private function loadUser(int $uid): User {
    $user = User::load($uid);
    if (!$user) {
      throw new NotFoundHttpException('User not found.');
    }
    return $user;
  }

  private function checkUserStatus(User $user): void {
    if ($user->isActive()) {
      throw new JsonResponse(['message' => 'User is already active.'], 400);
    }
  }

  private function validateTokenTimestamp(int $timestamp): void {
    $token_expiration_time = $this->getTokenExpirationTime();

    $time_difference = time() - $timestamp;
    if ($time_difference > $token_expiration_time) {
      throw new JsonResponse(['message' => 'Token has expired.'], 400);
    }
  }

  private function activateUser(User $user): void {
    $user->set('status', 1);
    $user->save();
  }

}

Lorsqu’une requête GET est envoyée avec un token, le contrôleur commence par extraire ce token à l’aide de la méthode privée getTokenFromRequest, qui vérifie sa présence dans la requête et déclenche une exception si le token est absent. Ensuite, le token est décodé via la méthode parseToken, qui utilise une expression régulière pour extraire l’ID utilisateur (UID) et un timestamp, nécessaires pour valider le token.

Une fois le token décodé, la méthode loadUser tente de charger l’utilisateur correspondant à l’UID fourni. Si aucun utilisateur n’est trouvé, une exception est levée. Ensuite, checkUserStatus vérifie si l’utilisateur est déjà actif. Si l’utilisateur est actif, une réponse JSON est renvoyée avec un message d’erreur. Si l’utilisateur est inactif, la méthode validateTokenTimestamp vérifie que le token n’a pas expiré en comparant le timestamp du token avec l’heure actuelle. Cette vérification utilise la valeur définie dans la configuration Drupal via le paramètre password_reset_timeout, qui spécifie la durée d’expiration des tokens pour des opérations liées à l’authentification des utilisateurs, comme la réinitialisation de mot de passe. Si le token n’est pas encore expiré, la méthode activateUser active le compte de l’utilisateur en mettant à jour son statut dans la base de données.

Création d’une API pour la Réémission du Lien

Dans certains cas, des utilisateurs peuvent ne pas avoir activé leur compte dans les délais impartis ou ne pas avoir reçu l’email d’activation pour diverses raisons (erreur d’adresse, placement dans le dossier spam, etc.). Pour ces utilisateurs, il est utile de fournir un moyen de renvoyer le lien d’activation. Nous allons mettre en place une API qui permettra aux utilisateurs de demander la réémission de ce lien. Pour cela, nous commençons par ajouter une nouvelle route dans le fichier custom_auth.routing.yml, afin de permettre l’accès à cette API. Cette route sera utilisée pour générer un nouveau lien d’activation et l’envoyer à l’utilisateur.

custom_auth.resend_activation_link:
  path: '/api/resend-activation-link'
  defaults:
    _controller: '\Drupal\custom_auth\Controller\ActivationResendController::resend'
    _title: 'Resend Activation Link'
  methods: [POST]
  requirements:
    _permission: 'access content'

Comme pour l’API d’activation initiale, il est important de protéger l’API de réémission du lien d’activation. Nous allons ajouter une nouvelle permission dans custom_auth.permissions.yml pour limiter l’accès à cette fonctionnalité.

access resend activation link api:
  title: 'Access API to resend activation link'
  description: 'Allows users to access the API to request a new activation link.'
  restrict access: TRUE

Après avoir défini la route et les permissions, nous devons créer un contrôleur pour gérer la logique derrière cette fonctionnalité. Ce contrôleur recevra la requête d’un utilisateur, vérifiera si l’utilisateur est éligible pour recevoir un nouveau lien, et ensuite renverra un email contenant le lien d’activation.

Voici un exemple de contrôleur nommé ActivationResendController que vous pouvez placer dans le répertoire src/Controller de votre module personnalisé.

<?php

namespace Drupal\custom_auth\Controller;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Drupal\user\Entity\User;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Mail\MailManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Controller to handle the resending of activation links via API.
 */
class ActivationResendController extends ControllerBase {

  /**
   * The mail manager service.
   *
   * @var \Drupal\Core\Mail\MailManagerInterface
   */
  protected $mailManager;

  /**
   * Constructs an ActivationResendController object.
   *
   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
   *   The mail manager service.
   */
  public function __construct(MailManagerInterface $mail_manager) {
    $this->mailManager = $mail_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): self {
    return new static(
      $container->get('plugin.manager.mail')
    );
  }

  /**
   * Resends the activation link to the user.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   A JSON response indicating success or failure.
   */
  public function resend(Request $request): JsonResponse {
    $email = $request->request->get('mail');

    if ($this->isEmailEmpty($email)) {
      return $this->createErrorResponse('Mail address is required.', 400);
    }

    $user = $this->loadUserByEmail($email);
    if ($user === NULL) {
      return $this->createErrorResponse('User not found.', 404);
    }

    if ($this->isUserActive($user)) {
      return $this->createErrorResponse('Account is already active.', 400);
    }

    $this->sendActivationEmail($user);
    return $this->createSuccessResponse('Activation link sent successfully.');
  }

  /**
   * Checks if the email address is empty.
   *
   * @param string|null $email
   *   The email address.
   *
   * @return bool
   *   TRUE if the email address is empty; FALSE otherwise.
   */
  protected function isEmailEmpty(?string $email): bool {
    return empty($email);
  }

  /**
   * Loads the user by email.
   *
   * @param string $email
   *   The email address.
   *
   * @return \Drupal\user\Entity\User|null
   *   The user entity or NULL if not found.
   */
  protected function loadUserByEmail(string $email): ?User {
    $user_storage = \Drupal::entityTypeManager()->getStorage('user');
    $users = $user_storage->loadByProperties(['mail' => $email]);
    return reset($users) ?: NULL;
  }

  /**
   * Checks if the user is active.
   *
   * @param \Drupal\user\Entity\User $user
   *   The user entity.
   *
   * @return bool
   *   TRUE if the user is active; FALSE otherwise.
   */
  protected function isUserActive(User $user): bool {
    return $user->isActive();
  }

  /**
   * Creates an error response.
   *
   * @param string $message
   *   The error message.
   * @param int $status_code
   *   The HTTP status code.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   The JSON response.
   */
  protected function createErrorResponse(string $message, int $status_code): JsonResponse {
    return new JsonResponse(['message' => $message], $status_code);
  }

  /**
   * Creates a success response.
   *
   * @param string $message
   *   The success message.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   The JSON response.
   */
  protected function createSuccessResponse(string $message): JsonResponse {
    return new JsonResponse(['message' => $message]);
  }

  /**
   * Sends the activation email to the user.
   *
   * @param \Drupal\user\Entity\User $user
   *   The user entity.
   */
  protected function sendActivationEmail(User $user): void {
    $langcode = $user->getPreferredLangcode();
    $params = ['account' => $user];

    $result = $this->mailManager->mail('user', 'register_pending_approval', $user->getEmail(), $langcode, $params);
    
    if ($result['result'] !== TRUE) {
      $this->logEmailError($user->getEmail());
    } else {
      $this->logEmailSuccess($user->getEmail());
    }
  }

  /**
   * Logs an email error.
   *
   * @param string $email
   *   The email address.
   */
  protected function logEmailError(string $email): void {
    \Drupal::logger('custom_auth')->error('Failed to send activation email to ' . $email);
  }

  /**
   * Logs a successful email send.
   *
   * @param string $email
   *   The email address.
   */
  protected function logEmailSuccess(string $email): void {
    \Drupal::logger('custom_auth')->notice('Activation email sent to ' . $email);
  }
}

À la réception d’une requête, la méthode resend extrait l’adresse e-mail fournie par l’utilisateur et vérifie son existence à l’aide de la méthode isEmailEmpty. Si l’adresse e-mail est manquante, une réponse JSON indiquant que l’adresse est requise est renvoyée avec le code d’état 400. Ensuite, le contrôleur tente de charger l’utilisateur associé à cette adresse e-mail grâce à la méthode loadUserByEmail; si aucun utilisateur n’est trouvé, une réponse JSON signifiant que l’utilisateur est introuvable est retournée avec le code d’état 404. Si l’utilisateur est trouvé mais déjà actif, la méthode isUserActive renvoie une réponse similaire. Si l’utilisateur est inactif, le contrôleur appelle la méthode sendActivationEmail pour envoyer un e-mail d’activation, puis renvoie une réponse JSON confirmant l’envoi de l’e-mail via la méthode createSuccessResponse.

Test de l’API d’Activation

Pour effectuer un test de l’API d’activation de compte, vous devez suivre une série d’étapes claires qui permettent de valider le bon fonctionnement du processus d’inscription et d’activation. Tout d’abord, créez un nouvel utilisateur via le formulaire d’inscription disponible sur votre site Drupal. Dès que l’utilisateur est enregistré, un email d’activation est automatiquement envoyé à l’adresse fournie. Cet email contient un lien de validation comportant un paramètre essentiel : un token unique. Ce lien doit ressembler à quelque chose du type :

https://your-frontend-url.com/activate?token=<encoded_link>

Le token encodé dans l’URL est une clé qui permet d’activer le compte de l’utilisateur. Pour simuler l’activation, vous devrez extraire ce token de l’email reçu. Ensuite, à l’aide d’un navigateur ou d’un outil dédié comme Apidog, vous pouvez faire un appel direct à l’API de votre backend en envoyant une requête GET à l’URL suivante :

https://your-backend-url.com/api/activate-account?token=<encoded_link>

L’API retournera alors une réponse en format JSON pour confirmer si l’activation a réussi. Si le token est valide, vous recevrez un message comme celui-ci :

{
  "message": "Account activated successfully."
}

En revanche, si le token est invalide ou expiré, un message d’erreur sera retourné, par exemple :

{
  "message": "Token has expired."
}

Pour vous assurer que le processus a abouti, vous pouvez également vérifier le statut de l’utilisateur dans l’interface d’administration de Drupal. Le compte doit être marqué comme “Actif”, confirmant ainsi que l’activation s’est bien déroulée et que l’utilisateur peut désormais se connecter au site. Ce processus garantit que chaque étape de l’inscription et de l’activation est bien validée, et il permet de déceler d’éventuels problèmes avant le déploiement en production.

Test de l’API de Réémission du Lien

Pour tester la fonctionnalité de réémission du lien d’activation après avoir mis en place l’API, il est important de suivre quelques étapes spécifiques. D’abord, commencez par créer un utilisateur via le formulaire d’inscription, en veillant à ce que cet utilisateur reste inactif après l’inscription. Ensuite, envoyez une requête POST à l’API de réémission du lien d’activation, disponible à l’adresse suivante : https://your-backend-url.com/api/resend-activation-link. Utilisez l’adresse e-mail de l’utilisateur non activé dans la requête, en vous assurant que le corps de la requête soit encodé en x-www-form-urlencoded.

Après l’envoi de cette requête, vérifiez si l’utilisateur reçoit un nouvel email contenant un lien d’activation. Le lien dans cet email doit rediriger correctement l’utilisateur vers le frontend pour l’activation du compte. Si tout fonctionne correctement, la réponse de l’API devrait être un message JSON du type :

{
  "message": "Activation link resent successfully."
}

Si l’utilisateur est déjà activé, la réponse sera différente :

{
  "message": "User is already active."
}

En cas d’erreur, par exemple si l’utilisateur n’existe pas ou si l’email n’a pas été fourni, l’API doit retourner une réponse appropriée. Cela peut inclure une erreur 404 si l’utilisateur n’a pas été trouvé, ou une erreur 400 si l’utilisateur est déjà activé.

Conclusion

Nous avons mis en place un processus efficace et sécurisé pour gérer l’activation des comptes utilisateurs dans un environnement découplé. Ce mécanisme permet aux utilisateurs de recevoir un lien d’activation par email, les redirigeant directement vers le frontend, garantissant une expérience utilisateur fluide et intuitive. L’API dédiée côté backend assure un traitement sécurisé du token d’activation, en suivant les bonnes pratiques de sécurité. De plus, l’API de réémission du lien d’activation permet de renvoyer facilement le lien aux utilisateurs inactifs, renforçant ainsi la flexibilité du système. Ce processus global optimise la gestion des activations de comptes, tout en respectant les principes d’une architecture découplée et en améliorant la séparation des responsabilités entre le frontend et le backend.

Développeuse fullstack * Plus de publications

Développeuse fullstack spécialisée en Systèmes d'Information Répartis , diplômée de la section informatique de la Faculté des Sciences et Techniques (FST) de l'UCAD. Je relève avec passion des défis complexes, avec une forte capacité d'adaptation et un engagement pour la collaboration en équipe. Toujours avide d'apprendre, je vise à créer un impact positif et à promouvoir l'excellence organisationnelle dans chaque projet.

Contributeurs

0 0 votes
Évaluation de l'article
guest
0 Commentaires
Le plus ancien
Le plus récent Le plus populaire
Commentaires en ligne
Afficher tous les commentaires

Ingénierie informatique (SSII)

Applize crée des logiciels métiers pour accompagner les entreprises dans la transition vers le zéro papier.


Avez-vous un projet en tête ? Discutons-en.