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.
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
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.
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
Nodemailer : Módulo poderoso que permite enviar e-mails facilmente usando SMTP ou outros mecanismos de transporte.
Guidão : 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.
Agora, precisamos criar a migração, então no meu caso, tenho que adicionar uma nova coluna forgot_password_token
à tabela 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 users
nosso forgot_password_token
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 forgotPasswordRouter.ts
dentro do diretório src/routes/
e insira o seguinte 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);
Esqueci a senha do roteador
Dois controladores gerenciarão a lógica de envio de e-mails e redefinição de senha.
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 UserModel.findByEmail
. Se o usuário existir, criamos um token JWT usando TokenService.sign
e salvamos o token no usuário forgot_password_token
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.
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 Gerenciar sua conta do Google . Você pode encontrá-lo no canto superior direito clicando no avatar. Em seguida, no menu esquerdo, clique no item Segurança e pressione Verificação em duas etapas . Abaixo você encontrará a seção Senhas de aplicativos , clique na seta:
Insira o nome que precisa ser usado. No meu caso, configurei Nodemailer
e pressione Create .
Copie a senha gerada e defina-a em seu arquivo .env
. Precisamos definir o arquivo de duas variáveis:
MAIL_USER="[email protected]" MAIL_PASSWORD="vyew hzek avty iwst"
Claro, para ter um e-mail adequado como info@company_name.com
, 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.
Com o arquivo .env
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.
await EmailService.sendPasswordResetEmail(email, token);
Vamos criar src/services/EmailService.ts
e definir a classe do serviço:
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
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 nodemailer.createTransport()
, um método fornecido pela biblioteca nodemailer
. 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.
Estamos usando o Google: service: 'gmail'
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 gmail
indica que o transportador será configurado para funcionar com o servidor SMTP do Gmail.
Para autenticação auth
, é necessário definir as credenciais necessárias para acessar o servidor SMTP do provedor de serviços de e-mail.
Para user
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.
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 handlebars
e para isso precisamos criar em src/temlates/passwordResetTemplate.hbs
nosso primeiro template 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>
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 email_logo.png
à pasta src/assets
. Podemos então renderizar esta imagem no e-mail usando a seguinte função 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, }; });
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
O console informará que a mensagem foi enviada:
Message sent: <1k96ah55-c09t-p9k2–[email protected]>
Verifique se há novas mensagens na caixa de entrada:
O link para Redefinir Senha deve conter o token e o host:
http://localhost:3000/reset-password/<token>
A porta 3000
é 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.
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 src/controllers/
e crie um arquivo chamado resetPasswordController.ts
contendo o seguinte 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); } };
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.
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
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.
Aqui, você pode encontrar várias referências que utilizei neste artigo:
Também publicado aqui