Este es un análisis detallado de cómo realizar un reseteo a un usuario cuando ha olvidado su contraseña y cómo enviar correos electrónicos desde Node JS y validar el envío de mensajes. La mayoría de nosotros hemos experimentado el proceso de recuperación de cuenta al menos una vez: cuando olvidamos una contraseña, es necesario realizar procedimientos para crear una nueva y recuperar el acceso al sistema. Este artículo se centra en la implementación de dicho proceso utilizando Node.js, Knex y algunas herramientas no reveladas, junto con Express para manejar rutas y realizar las operaciones necesarias. Cubriremos la implementación del enrutador, el manejo de los parámetros de URL, la determinación de qué enviar al usuario cuando solo hay un correo electrónico o un número de teléfono disponible como prueba, la gestión de envíos de correo electrónico y la resolución de problemas de seguridad. Flujo de contraseña olvidada Antes de sumergirme en la codificación, me gustaría asegurarme de que estamos trabajando con la misma base de código, a la que puedes acceder desde mi sitio público. . Actualizaremos paso a paso para implementar el flujo de contraseña olvidada. Para el transporte de correo electrónico, utilizaremos el servicio de correo electrónico de Google. repositorio en GitHub Ahora, eche un vistazo al esquema del flujo de contraseña olvidada. El servidor será responsable de enviar correos electrónicos al buzón del usuario que contengan un enlace válido para el restablecimiento de la contraseña, y también validará el token y la existencia del usuario. Paquetes y Migración Para comenzar a utilizar el servicio de correo electrónico y enviar correos electrónicos con Node.js, necesitamos instalar los siguientes paquetes además de nuestras dependencias existentes: npm i --save nodemailer handlebars : Potente módulo que permite enviar correos electrónicos fácilmente utilizando SMTP u otros mecanismos de transporte. Nodemailer : Manillar es un motor de plantillas popular para JavaScript. Nos permitirá definir plantillas con marcadores de posición que se pueden llenar con datos al renderizar. Manillar Ahora, necesitamos crear la migración, así que en mi caso, tengo que agregar una nueva columna a la tabla : forgot_password_token users knex migrate:make add_field_forgot_password_token -x ts y en el archivo generado, configuro el código: 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'); }); } Migración del token de contraseña olvidada en la tabla Usuarios y luego migrar el archivo más reciente: knex migrate:knex Ahora podemos configurar en la tabla nuestro users forgot_password_token Enrutadores Para gestionar los controladores encargados de manejar la lógica de olvido y restablecimiento de contraseña, debemos establecer dos rutas. La primera ruta inicia el proceso de olvido de contraseña, mientras que la segunda maneja el proceso de restablecimiento, esperando un parámetro token en la URL para su verificación. Para implementar esto, cree un archivo llamado dentro del directorio e inserte el siguiente código: forgotPasswordRouter.ts src/routes/ 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); Olvidé mi contraseña del enrutador Dos controladores gestionarán la lógica para enviar correos electrónicos y restablecer la contraseña. Olvidé mi contraseña del controlador Cuando el cliente olvida su contraseña, no tiene sesión, lo que significa que no podemos obtener datos del usuario excepto el correo electrónico o cualquier otro identificador de seguridad. En nuestro caso, enviamos un correo electrónico para gestionar el restablecimiento de contraseña. Esa lógica la vamos a configurar en el controlador. forgotPasswordRouter.post('/', forgotPasswordController); ¿Recuerdas el mensaje "¿Olvidaste tu contraseña?" ¿Enlace debajo del formulario de inicio de sesión generalmente en la interfaz de usuario de cualquier cliente en el formulario de inicio de sesión? Al hacer clic en él nos dirige a una vista donde podemos solicitar un restablecimiento de contraseña. Simplemente ingresamos nuestro correo electrónico y el controlador se encarga de todos los procedimientos necesarios. Examinemos el siguiente código: 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); } }; Olvidé mi contraseña del controlador Del cuerpo, recibiremos un correo electrónico y luego encontraremos al usuario usando . Si el usuario existe, creamos un token JWT usando y guardamos el token en el usuario con una caducidad de 1 día. Luego enviaremos el mensaje al correo electrónico con un enlace adecuado junto con un token donde el usuario podrá cambiar su contraseña. UserModel.findByEmail TokenService.sign forgot_password_token Configuración de Google Para poder enviar el correo electrónico, tenemos que crear nuestra nueva dirección de correo electrónico que será un remitente. Vayamos a Google para crear una nueva cuenta de correo electrónico y luego, cuando se cree la cuenta, vayamos al enlace . Puedes encontrarlo en la parte superior derecha haciendo clic en avatar. Luego, en el menú de la izquierda, haga clic en el elemento y luego presione . A continuación encontrarás la sección , haz clic en la flecha: Administrar su cuenta de Google Seguridad Verificación en 2 pasos Contraseñas de aplicaciones Ingrese el nombre que debe usarse. En mi caso, configuro y presiono . Nodemailer Crear Copie la contraseña generada y configúrela en su archivo . Necesitamos configurar para presentar dos variables: .env MAIL_USER="mygoogleemail@gmail.com" MAIL_PASSWORD="vyew hzek avty iwst" Por supuesto, para tener un correo electrónico adecuado como , debe configurar Google Workspace o AWS Amazon WorkMail junto con AWS SES o cualquier otro servicio. Pero en nuestro caso, utilizamos una simple cuenta de Gmail de forma gratuita. info@company_name.com Servicio de correo electrónico Con el archivo preparado, estamos listos para configurar nuestro servicio de envío de correos electrónicos. El controlador utilizará el servicio con el token generado y la dirección de correo electrónico del destinatario de nuestro mensaje. .env await EmailService.sendPasswordResetEmail(email, token); Creemos y definamos la clase para el servicio: src/services/EmailService.ts export class EmailService {} Y ahora como dato inicial, tengo que conseguir el entorno para utilizarlo con : 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, }; } Servicio de correo electrónico Tenemos que encargarnos de la inicialización del servicio. Escribí sobre esto antes en mi anterior. . Aquí hay un ejemplo: artículo 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(); }; Inicializando servicios Ahora, procedamos a crear la inicialización dentro de nuestra clase : 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; } } } Inicialización del servicio de correo electrónico Hay una inicialización , un método proporcionado por la biblioteca . Crea un objeto transportador que se utilizará para enviar nuestros correos electrónicos. El método acepta un objeto de opciones como argumento donde se especifican los detalles de configuración del transportador. nodemailer.createTransport() nodemailer Estamos utilizando Google: especifica el proveedor del servicio de correo electrónico. Nodemailer proporciona soporte integrado para varios proveedores de servicios de correo electrónico y indica que el transportador se configurará para funcionar con el servidor SMTP de Gmail. service: 'gmail' gmail Para la , es necesario configurar las credenciales necesarias para acceder al servidor SMTP del proveedor de servicios de correo electrónico. auth Para se debe configurar la dirección de correo electrónico desde la que vamos a enviar correos electrónicos, y esa contraseña se ha generado en la cuenta de Google desde App Passwords. user Ahora, configuremos la última parte de nuestro servicio: 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); } } } Enviar correo electrónico para restablecer contraseña Antes de continuar, es fundamental determinar el host adecuado para cuando el cliente reciba un correo electrónico. Establecer un enlace con un token en el cuerpo del correo electrónico es fundamental. 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}`; }; conseguir anfitrión Para las plantillas, estoy usando y para eso, necesitamos crear en nuestra primera plantilla HTML: handlebars src/temlates/passwordResetTemplate.hbs <!-- 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> Plantilla de restablecimiento de contraseña y ahora podemos reutilizar esta plantilla con el ayudante: 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); }; Generar ayudante de plantilla Para mejorar nuestro correo electrónico, incluso podemos incluir archivos adjuntos. Para hacerlo, agregue el archivo a la carpeta . Luego podemos representar esta imagen dentro del correo electrónico usando la siguiente función auxiliar: email_logo.png src/assets 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, }; }); Ayudante para generar archivos adjuntos Después de recopilar todos esos ayudantes, debemos poder enviar correos electrónicos usando: const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, }); Este enfoque ofrece una escalabilidad decente, lo que permite que el servicio emplee varios métodos para enviar correos electrónicos con contenido diverso. Ahora, intentemos activar el controlador con nuestro enrutador y enviar el correo electrónico. Para eso estoy usando : Cartero La consola te dirá que el mensaje ha sido enviado: Message sent: <1k96ah55-c09t-p9k2–8bv2-j25r9h77f763@gmail.com> Verifique si hay mensajes nuevos en la bandeja de entrada: El enlace para debe contener el token y el host: restablecer contraseña http://localhost:3000/reset-password/<token> El puerto se especifica aquí porque este mensaje pertenece al proceso de desarrollo. Esto indica que el cliente responsable de manejar los formularios para restablecer la contraseña también estará operando dentro del entorno de desarrollo. 3000 Restablecer la contraseña El token debe validarse en el lado del controlador con TokenService desde donde podemos obtener el usuario que envió ese correo electrónico. Recuperemos el enrutador que usa el token: forgotPasswordRouter.post('/reset/:token', resetPasswordController); El controlador solo actualizará la contraseña si el token es válido y no ha caducado, según el tiempo de caducidad establecido en una hora. Para implementar esta funcionalidad, navegue hasta la carpeta y cree un archivo llamado que contenga el siguiente código: src/controllers/ resetPasswordController.ts 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); } }; Restablecer contraseña del controlador Este controlador recibirá el token, lo verificará, extraerá la ID de usuario de los datos descifrados, recuperará el usuario correspondiente, adquirirá la nueva contraseña enviada por el cliente en el cuerpo de la solicitud y procederá a actualizar la contraseña en la base de datos. En última instancia, esto permite al cliente iniciar sesión con la nueva contraseña. Conclusión La escalabilidad del servicio de correo electrónico se demuestra a través de varios enfoques, como el envío de confirmaciones o mensajes de éxito, como los que indican la actualización de la contraseña y permiten el inicio de sesión posterior. Sin embargo, administrar contraseñas es un gran desafío, especialmente cuando es imperativo mejorar la seguridad de las aplicaciones. Hay numerosas opciones disponibles para reforzar la seguridad, incluidas comprobaciones adicionales antes de permitir cambios de contraseña, como comparación de tokens, correo electrónico y validación de contraseñas. Otra opción es implementar un sistema de código PIN, donde se envía un código al correo electrónico del usuario para su validación en el lado del servidor. Cada una de estas medidas requiere la utilización de capacidades de envío de correo electrónico. Todo el código implementado lo puedes encontrar en el . Repositorio de GitHub aquí No dude en realizar cualquier experimento con esta compilación y compartir sus comentarios sobre los aspectos que aprecia de este tema. Muchas gracias. Referencias Aquí puede encontrar varias referencias que utilicé en este artículo: Repositorio Servicios de inicialización con Node JS Nodemailer Bigote daliniano Knex Expresar Cartero También publicado aquí
Share Your Thoughts