La plupart d'entre nous ont expérimenté le processus de récupération de compte au moins une fois : lorsque nous oublions un mot de passe, des procédures sont nécessaires pour en créer un nouveau et retrouver l'accès au système. Cet article se concentre sur la mise en œuvre d'un tel processus à l'aide de Node.js, Knex et de certains outils non divulgués, aux côtés d'Express, pour gérer les routes et effectuer les opérations nécessaires.
Nous couvrirons la mise en œuvre du routeur, la gestion des paramètres d'URL, la détermination de ce qu'il faut envoyer à l'utilisateur lorsque seul un e-mail ou un numéro de téléphone est disponible comme preuve, la gestion des envois d'e-mails et la résolution des problèmes de sécurité.
Avant de me lancer dans le codage, j'aimerais m'assurer que nous travaillons avec la même base de code, à laquelle vous pouvez accéder depuis mon compte public.
Maintenant, jetez un œil au schéma du flux de mot de passe oublié.
Le serveur se chargera d'envoyer des e-mails à la boîte aux lettres de l'utilisateur contenant un lien valide pour la réinitialisation du mot de passe, et validera également le jeton et l'existence de l'utilisateur.
Pour commencer à utiliser le service de messagerie et à envoyer des e-mails avec Node.js, nous devons installer les packages suivants en plus de nos dépendances existantes :
npm i --save nodemailer handlebars
Nodemailer : Module puissant qui permet d'envoyer facilement des emails en utilisant SMTP ou d'autres mécanismes de transport.
Guidon : Guidon est un moteur de création de modèles populaire pour JavaScript. Cela nous permettra de définir des modèles avec des espaces réservés qui pourront être remplis de données lors du rendu.
Maintenant, nous devons créer la migration, donc dans mon cas, je dois ajouter une nouvelle colonne forgot_password_token
à la table users
:
knex migrate:make add_field_forgot_password_token -x ts
et dans le fichier généré, je mets le code :
import type { Knex } from 'knex'; export async function up(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.string('forgot_password_token').unique(); }); } export async function down(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.dropColumn('forgot_password_token'); }); }
Migration du jeton de mot de passe oublié dans le tableau Utilisateurs
puis migrez le dernier fichier :
knex migrate:knex
Alors maintenant, nous pouvons définir dans la table users
notre forgot_password_token
Pour gérer les contrôleurs chargés de gérer la logique d’oubli et de réinitialisation des mots de passe, nous devons établir deux routes. La première route lance le processus de mot de passe oublié, tandis que la seconde gère le processus de réinitialisation, en attendant un paramètre de jeton dans l'URL pour vérification. Pour implémenter cela, créez un fichier nommé forgotPasswordRouter.ts
dans le répertoire src/routes/
et insérez le code suivant :
import { Router } from 'express'; import { forgotPasswordController } from 'src/controllers/forgotPasswordController'; import { resetPasswordController } from 'src/controllers/resetPasswordController'; export const forgotPasswordRouter = Router(); forgotPasswordRouter.post('/', forgotPasswordController); forgotPasswordRouter.post('/reset/:token', resetPasswordController);
Mot de passe oublié du routeur
Deux contrôleurs géreront la logique d'envoi des emails et de réinitialisation du mot de passe.
Lorsque le client oublie son mot de passe, il n'a pas de session, ce qui signifie que nous ne pouvons pas obtenir de données utilisateur, à l'exception du courrier électronique ou de tout autre identifiant de sécurité. Dans notre cas, nous envoyons un e-mail pour gérer une réinitialisation de mot de passe. Cette logique, nous allons l'intégrer au contrôleur.
forgotPasswordRouter.post('/', forgotPasswordController);
Vous vous souvenez du « mot de passe oublié ? » lien sous le formulaire de connexion, généralement dans l'interface utilisateur de tous les clients dans le formulaire de connexion ? En cliquant dessus, nous sommes dirigés vers une vue où nous pouvons demander une réinitialisation du mot de passe. Nous saisissons simplement notre e-mail et le contrôleur gère toutes les procédures nécessaires. Examinons le code suivant :
import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; import { TokenService } from 'src/services/TokenService'; import { EmailService } from 'src/services/EmailService'; export const forgotPasswordController = async (req: Request, res: Response) => { try { const { email, }: { email: string; } = req.body; const user = await UserModel.findByEmail(email); if (user) { const token = await TokenService.sign( { id: user.id, }, { expiresIn: '1 day', } ); await user.context.update({ forgot_password_token: token }); await EmailService.sendPasswordResetEmail(email, token); } return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } };
Contrôleur de mot de passe oublié
À partir du corps, nous allons recevoir un e-mail, puis nous trouverons l'utilisateur en utilisant UserModel.findByEmail
. Si l'utilisateur existe, nous créons un jeton JWT à l'aide TokenService.sign
et enregistrons le jeton dans l'utilisateur forgot_password_token
avec une expiration d'un jour. Ensuite, nous enverrons le message à l'e-mail avec un lien approprié ainsi qu'un jeton où l'utilisateur pourra changer son mot de passe.
Pour pouvoir envoyer l'e-mail, nous devons créer notre nouvelle adresse e-mail qui sera expéditeur.
Allons sur Google, pour créer un nouveau compte de messagerie, puis, une fois le compte créé, passez au lien Gérer votre compte Google . Vous pouvez le trouver en haut à droite en cliquant sur avatar. Ensuite, dans le menu de gauche, cliquez sur l'élément Sécurité , puis appuyez sur Vérification en 2 étapes . Ci-dessous vous trouverez la section Mots de passe des applications , cliquez sur la flèche :
Saisissez le nom qui doit être utilisé. Dans mon cas, je configure Nodemailer
et j'appuie sur Create .
Copiez le mot de passe généré et définissez-le dans votre fichier .env
. Nous devons définir deux variables dans un fichier :
MAIL_USER="[email protected]" MAIL_PASSWORD="vyew hzek avty iwst"
Bien sûr, pour avoir une adresse e-mail appropriée comme info@company_name.com
, vous devez configurer Google Workspace ou AWS Amazon WorkMail avec AWS SES, ou tout autre service. Mais dans notre cas, nous utilisons gratuitement un simple compte Gmail.
Une fois le fichier .env
préparé, nous sommes prêts à configurer notre service d'envoi d'e-mails. Le contrôleur utilisera le service avec le jeton généré et l'adresse e-mail du destinataire de notre message.
await EmailService.sendPasswordResetEmail(email, token);
Créons src/services/EmailService.ts
et définissons la classe du service :
export class EmailService {}
Et maintenant, comme données initiales, je dois utiliser l'environnement avec nodemailer
:
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; }
Service de courrier électronique
Nous devons nous occuper de l’initialisation du service. J'en ai déjà parlé dans mon précédent
import { TokenService } from 'src/services/TokenService'; import { RedisService } from 'src/services/RedisService'; import { EmailService } from 'src/services/EmailService'; export const initialize = async () => { await RedisService.initialize(); TokenService.initialize(); EmailService.initialize(); };
Initialisation des services
Passons maintenant à la création de l'initialisation au sein de notre classe EmailService
:
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; public static initialize() { try { EmailService.transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: this.env.USER, pass: this.env.PASS, }, }); } catch (error) { console.error('Error initializing email service'); throw error; } } }
Initialisation du service de messagerie
Il existe une initialisation nodemailer.createTransport()
, une méthode fournie par la bibliothèque nodemailer
. Il crée un objet transporteur qui sera utilisé pour envoyer nos emails. La méthode accepte un objet options comme argument dans lequel vous spécifiez les détails de configuration du transporteur.
Nous utilisons Google : service: 'gmail'
précise le fournisseur de service de messagerie. Nodemailer fournit une prise en charge intégrée pour divers fournisseurs de services de messagerie, et gmail
indique que le transporteur sera configuré pour fonctionner avec le serveur SMTP de Gmail.
Pour l' auth
, il est nécessaire de définir les informations d'identification requises pour accéder au serveur SMTP du fournisseur de services de messagerie.
Pour user
l'adresse e-mail à partir de laquelle nous allons envoyer des e-mails doit être définie, et ce mot de passe a été généré dans le compte Google à partir des mots de passe d'application.
Maintenant, définissons la dernière partie de notre service :
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; import { generateAttachments } from 'src/helpers/generateAttachments'; import { generateTemplate } from 'src/helpers/generateTemplate'; import { getHost } from 'src/helpers/getHost'; dotenv.config(); export class EmailService { // ...rest code public static async sendPasswordResetEmail(email: string, token: string) { try { const host = getHost(); const template = generateTemplate<{ token: string; host: string; }>('passwordResetTemplate', { token, host }); const attachments = generateAttachments([{ name: 'email_logo' }]); const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, }); console.log('Message sent: %s', info.messageId); } catch (error) { console.error('Error sending email: ', error); } } }
Envoyer un e-mail de réinitialisation du mot de passe
Avant de continuer, il est crucial de déterminer l'hébergeur approprié lorsque le client reçoit un e-mail. Établir un lien avec un token dans le corps de l’email est essentiel.
import * as dotenv from 'dotenv'; import process from 'process'; dotenv.config(); export const getHost = (): string => { const isProduction = process.env.NODE_ENV === 'production'; const protocol = isProduction ? 'https' : 'http'; const port = isProduction ? '' : `:${process.env.CLIENT_PORT}`; return `${protocol}://${process.env.WEB_HOST}${port}`; };
Obtenir un hôte
Pour les modèles, j'utilise handlebars
et pour cela, nous devons créer dans src/temlates/passwordResetTemplate.hbs
notre premier modèle HTML :
<!-- passwordResetTemplate.hbs --> <html lang='en'> <head> <style> a { color: #372aff; } .token { font-weight: bold; } </style> <title>Forgot Password</title> </head> <body> <p>You requested a password reset. Please use the following link to reset your password:</p> <a class='token' href="{{ host }}/reset-password/{{ token }}">Reset Password</a> <p>If you did not request a password reset, please ignore this email.</p> <img src="cid:email_logo" alt="Email Logo"/> </body> </html>
Modèle de réinitialisation de mot de passe
et maintenant nous pouvons réutiliser ce modèle avec l'assistant :
import path from 'path'; import fs from 'fs'; import handlebars from 'handlebars'; export const generateTemplate = <T>(name: string, props: T): string => { const templatePath = path.join(__dirname, '..', 'src/templates', `${name}.hbs`); const templateSource = fs.readFileSync(templatePath, 'utf8'); const template = handlebars.compile(templateSource); return template(props); };
Générer un assistant de modèle
Pour améliorer notre courrier électronique, nous pouvons même inclure des pièces jointes. Pour ce faire, ajoutez le fichier email_logo.png
au dossier src/assets
. Nous pouvons ensuite restituer cette image dans l'e-mail en utilisant la fonction d'assistance suivante :
import path from 'path'; import { Extension } from 'src/@types/enums'; type AttachmentFile = { name: string; ext?: Extension; cid?: string; }; export const generateAttachments = (files: AttachmentFile[] = []) => files.map(file => { const ext = file.ext || Extension.png; const filename = `${file.name}.${ext}`; const imagePath = path.join(__dirname, '..', 'src/assets', filename); return { filename, path: imagePath, cid: file.cid || file.name, }; });
Aide à la génération de pièces jointes
Après avoir collecté toutes ces aides, nous devons pouvoir envoyer des e-mails en utilisant :
const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, });
Cette approche offre une évolutivité décente, permettant au service d'utiliser diverses méthodes pour envoyer des e-mails avec un contenu diversifié.
Essayons maintenant de déclencher le contrôleur avec notre routeur et d'envoyer l'e-mail. Pour cela, j'utilise
La console vous indiquera que le message a été envoyé :
Message sent: <1k96ah55-c09t-p9k2–[email protected]>
Vérifiez les nouveaux messages dans la boîte de réception :
Le lien vers Réinitialiser le mot de passe doit contenir le jeton et l'hôte :
http://localhost:3000/reset-password/<token>
Le port 3000
est spécifié ici car ce message concerne le processus de développement. Cela indique que le client responsable du traitement des formulaires de réinitialisation de mot de passe fonctionnera également dans l'environnement de développement.
Le jeton doit être validé côté contrôleur avec TokenService d'où nous pouvons obtenir l'utilisateur qui a envoyé cet e-mail. Récupérons le routeur qui utilise le token :
forgotPasswordRouter.post('/reset/:token', resetPasswordController);
Le contrôleur ne mettra à jour le mot de passe que si le jeton est valide et n'a pas expiré, conformément au délai d'expiration fixé à une heure. Pour implémenter cette fonctionnalité, accédez au dossier src/controllers/
et créez un fichier nommé resetPasswordController.ts
contenant le code suivant :
import bcrypt from 'bcrypt'; import { Request, Response } from 'express'; import { TokenService } from 'src/services/TokenService'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; export const resetPasswordController = async (req: Request, res: Response) => { try { const token = req.params.token; if (!token) { return res.sendStatus(400); } const userData = await TokenService.verify<{ id: number }>(token); const user = await UserModel.findOneById<User>(userData.id); if (!user) { return res.sendStatus(400); } const newPassword = req.body.password; if (!newPassword) { return res.sendStatus(400); } const hashedPassword = await bcrypt.hash(newPassword, 10); await UserModel.updateById(user.id, { password: hashedPassword, passwordResetToken: null }); return res.sendStatus(200); } catch (error) { const errors = ['jwt malformed', 'TokenExpiredError', 'invalid token']; if (errors.includes(error.message)) { return res.sendStatus(400); } return res.sendStatus(500); } };
Réinitialiser le contrôleur de mot de passe
Ce contrôleur recevra le jeton, le vérifiera, extraira l'identifiant de l'utilisateur des données déchiffrées, récupérera l'utilisateur correspondant, acquerra le nouveau mot de passe envoyé par le client dans le corps de la demande et procédera à la mise à jour du mot de passe dans la base de données. En fin de compte, cela permet au client de se connecter en utilisant le nouveau mot de passe.
L'évolutivité du service de messagerie est démontrée à travers diverses approches, telles que l'envoi de confirmations ou de messages de réussite, comme ceux indiquant une mise à jour du mot de passe et permettant une connexion ultérieure. Cependant, la gestion des mots de passe constitue un défi de taille, en particulier lorsqu'il est impératif d'améliorer la sécurité des applications.
Il existe de nombreuses options disponibles pour renforcer la sécurité, notamment des vérifications supplémentaires avant d'autoriser la modification du mot de passe, telles que la comparaison des jetons, l'e-mail et la validation du mot de passe.
Une autre option consiste à mettre en œuvre un système de code PIN, dans lequel un code est envoyé à la messagerie électronique de l'utilisateur pour validation côté serveur. Chacune de ces mesures nécessite l'utilisation de capacités d'envoi d'e-mails.
Tout le code implémenté que vous pouvez trouver dans le
N'hésitez pas à mener des expériences avec cette version et à partager vos commentaires sur les aspects que vous appréciez sur ce sujet. Merci beaucoup.
Ici, vous pouvez trouver plusieurs références que j'ai utilisées dans cet article :
Également publié ici