paint-brush
Un tutorial del proceso de recuperación de cuentas usando NodeJS con Knex y Expresspor@antonkalik
659 lecturas
659 lecturas

Un tutorial del proceso de recuperación de cuentas usando NodeJS con Knex y Express

por Anton Kalik12m2024/03/28
Read on Terminal Reader

Demasiado Largo; Para Leer

Esta guía explora el restablecimiento de contraseñas de usuario utilizando Nodemailer, Knex y Express. Cubre el envío de correos electrónicos para recuperar la contraseña y validar la entrega de mensajes.
featured image - Un tutorial del proceso de recuperación de cuentas usando NodeJS con Knex y Express
Anton Kalik HackerNoon profile picture
0-item

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. repositorio en GitHub . 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.


Ahora, eche un vistazo al esquema del flujo de contraseña olvidada.

Olvidé el esquema de flujo de contraseña

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


Nodemailer : Potente módulo que permite enviar correos electrónicos fácilmente utilizando SMTP u otros mecanismos de transporte.


Manillar : 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.


Ahora, necesitamos crear la migración, así que en mi caso, tengo que agregar una nueva columna forgot_password_token a la tabla 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 users nuestro 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 forgotPasswordRouter.ts dentro del directorio src/routes/ e inserte el siguiente código:

 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 UserModel.findByEmail . Si el usuario existe, creamos un token JWT usando TokenService.sign y guardamos el token en el usuario forgot_password_token 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.

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 Administrar su cuenta de Google . Puedes encontrarlo en la parte superior derecha haciendo clic en avatar. Luego, en el menú de la izquierda, haga clic en el elemento Seguridad y luego presione Verificación en 2 pasos . A continuación encontrarás la sección Contraseñas de aplicaciones , haz clic en la flecha:


Contraseñas de aplicaciones

Ingrese el nombre que debe usarse. En mi caso, configuro Nodemailer y presiono Crear .

Crear contraseña de aplicación

Copie la contraseña generada y configúrela en su archivo .env . Necesitamos configurar para presentar dos variables:

 MAIL_USER="[email protected]" MAIL_PASSWORD="vyew hzek avty iwst"


Por supuesto, para tener un correo electrónico adecuado como info@company_name.com , 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.

Servicio de correo electrónico

Con el archivo .env 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.

 await EmailService.sendPasswordResetEmail(email, token);


Creemos src/services/EmailService.ts y definamos la clase para el servicio:

 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. artículo . Aquí hay un ejemplo:

 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 nodemailer.createTransport() , un método proporcionado por la biblioteca nodemailer . 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.


Estamos utilizando Google: service: 'gmail' especifica el proveedor del servicio de correo electrónico. Nodemailer proporciona soporte integrado para varios proveedores de servicios de correo electrónico y gmail indica que el transportador se configurará para funcionar con el servidor SMTP de Gmail.


Para la auth , es necesario configurar las credenciales necesarias para acceder al servidor SMTP del proveedor de servicios de correo electrónico.


Para user 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.


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 handlebars y para eso, necesitamos crear en src/temlates/passwordResetTemplate.hbs nuestra primera plantilla 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>

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 email_logo.png a la carpeta src/assets . Luego podemos representar esta imagen dentro del correo electrónico usando la siguiente función auxiliar:


 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 :

Enviar correo electrónico del cartero

La consola te dirá que el mensaje ha sido enviado:

 Message sent: <1k96ah55-c09t-p9k2–[email protected]>


Verifique si hay mensajes nuevos en la bandeja de entrada:

Bandeja de entrada con un nuevo correo electrónico del servidor

El enlace para restablecer contraseña debe contener el token y el host:

 http://localhost:3000/reset-password/<token>


El puerto 3000 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.

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 src/controllers/ y cree un archivo llamado resetPasswordController.ts que contenga el siguiente código:

 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:


También publicado aquí