Esta é uma análise detalhada de como fazer um reset para um usuário quando ele esqueceu sua senha e como enviar e-mails do Node JS e validar o envio de mensagens. A maioria de nós já passou pelo processo de recuperação de conta pelo menos uma vez — quando esquecemos uma senha, são necessários procedimentos para criar uma nova e recuperar o acesso ao sistema. Este artigo se concentra na implementação de tal processo usando Node.js, Knex e algumas ferramentas não divulgadas, juntamente com Express para lidar com rotas e realizar as operações necessárias. Abordaremos a implementação do roteador, o tratamento de parâmetros de URL, a determinação do que enviar ao usuário quando apenas um e-mail ou número de telefone estiver disponível como prova, o gerenciamento de envios de e-mail e a abordagem de questões de segurança. Esqueci o fluxo da senha Antes de mergulhar na codificação, gostaria de garantir que estamos trabalhando com a mesma base de código, que você pode acessar em meu site público . Atualizaremos passo a passo para implementar o fluxo de esquecimento de senha. Para transporte de e-mail, utilizaremos o serviço de e-mail do Google. repositório no GitHub Agora, dê uma olhada no esquema do fluxo de senha esquecida. O servidor será responsável por enviar e-mails para a caixa de correio do usuário contendo um link válido para redefinição de senha, e também validará o token e a existência do usuário. Pacotes e Migração Para começar a utilizar o serviço de email e enviar emails com Node.js, precisamos instalar os seguintes pacotes além de nossas dependências existentes: npm i --save nodemailer handlebars : Módulo poderoso que permite enviar e-mails facilmente usando SMTP ou outros mecanismos de transporte. Nodemailer : Guidão é um mecanismo de modelagem popular para JavaScript. Isso nos permitirá definir modelos com espaços reservados que podem ser preenchidos com dados durante a renderização. Guidão Agora, precisamos criar a migração, então no meu caso, tenho que adicionar uma nova coluna à tabela : forgot_password_token users knex migrate:make add_field_forgot_password_token -x ts e no arquivo gerado, defini o 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'); }); } Migração para token de senha esquecida na tabela Usuários e migre o arquivo mais recente: knex migrate:knex Então agora podemos definir para a tabela nosso users forgot_password_token Roteadores Para gerenciar os controladores responsáveis por lidar com a lógica de esquecimento e redefinição de senhas, devemos estabelecer duas rotas. A primeira rota inicia o processo de esquecimento da senha, enquanto a segunda trata do processo de redefinição, esperando um parâmetro de token na URL para verificação. Para implementar isso, crie um arquivo chamado dentro do diretório e insira o seguinte 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); Esqueci a senha do roteador Dois controladores gerenciarão a lógica de envio de e-mails e redefinição de senha. Esqueci a senha do controlador Quando o cliente esquece sua senha ele não tem sessão, o que significa que não podemos obter dados do usuário, exceto e-mail ou quaisquer outros identificadores de segurança. No nosso caso, estamos enviando um e-mail para realizar a redefinição de senha. Essa lógica vamos definir no controlador. forgotPasswordRouter.post('/', forgotPasswordController); Lembra do 'esqueceu a senha?' link abaixo do formulário de login geralmente na interface do usuário de qualquer cliente no formulário de login? Clicar nele nos direciona para uma visualização onde podemos solicitar uma redefinição de senha. Basta inserir nosso e-mail e o controlador trata de todos os procedimentos necessários. Vamos examinar o seguinte 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); } }; Esqueci a senha do controlador Do corpo receberemos um e-mail e então encontraremos o usuário usando . Se o usuário existir, criamos um token JWT usando e salvamos o token no usuário com validade de 1 dia. Em seguida enviaremos a mensagem para o e-mail com um link próprio junto com um token onde o usuário poderá alterar sua senha. UserModel.findByEmail TokenService.sign forgot_password_token Configuração do Google Para podermos enviar o email, temos que criar o nosso novo endereço de email que será o remetente. Vamos ao Google, para criar uma nova conta de e-mail e, quando a conta for criada, prossiga para o link . Você pode encontrá-lo no canto superior direito clicando no avatar. Em seguida, no menu esquerdo, clique no item e pressione . Abaixo você encontrará a seção , clique na seta: Gerenciar sua conta do Google Segurança Verificação em duas etapas Senhas de aplicativos Insira o nome que precisa ser usado. No meu caso, configurei e pressione . Nodemailer Create Copie a senha gerada e defina-a em seu arquivo . Precisamos definir o arquivo de duas variáveis: .env MAIL_USER="mygoogleemail@gmail.com" MAIL_PASSWORD="vyew hzek avty iwst" Claro, para ter um e-mail adequado como , você deve configurar o Google Workspace ou o AWS Amazon WorkMail junto com o AWS SES ou qualquer outro serviço. Mas, no nosso caso, estamos usando uma conta simples do Gmail gratuitamente. info@company_name.com Serviço de e-mail Com o arquivo preparado, estamos prontos para configurar nosso serviço de envio de emails. O controlador utilizará o serviço com o token gerado e o endereço de e-mail do destinatário da nossa mensagem. .env await EmailService.sendPasswordResetEmail(email, token); Vamos criar e definir a classe do serviço: src/services/EmailService.ts export class EmailService {} E agora como dados iniciais, tenho que pegar o ambiente para usar com : 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, }; } Serviço de e-mail Temos que cuidar da inicialização do serviço. Eu escrevi sobre isso antes em meu anterior . Aqui está um exemplo: artigo 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 serviços Agora, vamos prosseguir com a criação da inicialização em nossa 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; } } } Inicialização do serviço de e-mail Existe a inicialização , um método fornecido pela biblioteca . Ele cria um objeto transportador que será utilizado para enviar nossos emails. O método aceita um objeto de opções como argumento onde você especifica os detalhes de configuração do transportador. nodemailer.createTransport() nodemailer Estamos usando o Google: especifica o provedor de serviços de e-mail. O Nodemailer fornece suporte integrado para vários provedores de serviços de e-mail, e indica que o transportador será configurado para funcionar com o servidor SMTP do Gmail. service: 'gmail' gmail Para autenticação , é necessário definir as credenciais necessárias para acessar o servidor SMTP do provedor de serviços de e-mail. auth Para deve ser definido o endereço de e-mail do qual enviaremos e-mails, e essa senha foi gerada na conta Google em App Passwords. user Agora, vamos definir a última parte do nosso serviço: 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 e-mail de redefinição de senha Antes de prosseguir, é crucial determinar o host apropriado para quando o cliente receber um email. Estabelecer um link com um token no corpo do email é essencial. 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}`; }; Obter anfitrião Para templates, estou usando e para isso precisamos criar em nosso primeiro template 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> Modelo de redefinição de senha e agora podemos reutilizar este modelo com o auxiliar: 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); }; Gerar auxiliar de modelo Para aprimorar nosso e-mail, podemos até incluir anexos. Para fazer isso, adicione o arquivo à pasta . Podemos então renderizar esta imagem no e-mail usando a seguinte função 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, }; }); Auxiliar de geração de anexos Depois de coletar todos esses ajudantes, teremos que poder enviar e-mails usando: const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, }); Essa abordagem oferece escalabilidade decente, permitindo que o serviço empregue diversos métodos de envio de e-mails com conteúdos diversos. Agora, vamos tentar acionar o controlador com nosso roteador e enviar o email. Para isso, estou usando : Carteiro O console informará que a mensagem foi enviada: Message sent: <1k96ah55-c09t-p9k2–8bv2-j25r9h77f763@gmail.com> Verifique se há novas mensagens na caixa de entrada: O link para deve conter o token e o host: Redefinir Senha http://localhost:3000/reset-password/<token> A porta é especificada aqui porque esta mensagem pertence ao processo de desenvolvimento. Isso indica que o cliente responsável pelo tratamento dos formulários de redefinição de senha também estará operando dentro do ambiente de desenvolvimento. 3000 Redefinir senha O token deve ser validado no lado do controlador com TokenService de onde podemos obter o usuário que enviou aquele email. Vamos recuperar o roteador que utiliza o token: forgotPasswordRouter.post('/reset/:token', resetPasswordController); O controlador somente atualizará a senha se o token for válido e não expirado, conforme o tempo de expiração definido para uma hora. Para implementar esta funcionalidade, navegue até a pasta e crie um arquivo chamado contendo o seguinte 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); } }; Redefinir controlador de senha Este controlador receberá o token, verificará, extrairá o ID do usuário dos dados descriptografados, recuperará o usuário correspondente, adquirirá a nova senha enviada pelo cliente no corpo da solicitação e procederá à atualização da senha no banco de dados. Em última análise, isso permite que o cliente faça login usando a nova senha. Conclusão A escalabilidade do serviço de e-mail é demonstrada através de diversas abordagens, como envio de confirmações ou mensagens de sucesso, como aquelas que indicam atualização de senha e habilitação de login posterior. No entanto, o gerenciamento de senhas é um grande desafio, principalmente quando é fundamental aumentar a segurança dos aplicativos. Existem inúmeras opções disponíveis para reforçar a segurança, incluindo verificações adicionais antes de permitir alterações de senha, como comparação de token, email e validação de senha. Outra opção é implementar um sistema de código PIN, onde um código é enviado ao e-mail do usuário para validação no servidor. Cada uma dessas medidas exige a utilização de recursos de envio de e-mail. Todo o código implementado você pode encontrar no . Repositório GitHub aqui Sinta-se à vontade para realizar quaisquer experimentos com esta compilação e compartilhar seus comentários sobre quais aspectos você aprecia neste tópico. Muito obrigado. Referências Aqui, você pode encontrar várias referências que utilizei neste artigo: Repositório Serviços de inicialização com Node JS Nodemailer Guidão Knex Expressar Carteiro Também publicado aqui