Construindo uma API com NestJS, PostgreSQL e Docker —Parte 5: Enviando emails de confirmação e recuperação de senha
Recentemente comecei a estudar Node pensei em desenvolver uma API com funcionalidades básicas, que praticamente todo projeto precisa, como forma de me familiarizar com a linguagem. Para me auxiliar nessa tarefa utilizei o framework NestJS. Nesta série de tutoriais vou compartilhar o que aprendi.
Série completa:
Irei atualizando essa lista com links para as devidas partes conforme eu for postando!
- Parte 1: Criando nosso primeiro endpoint
- Parte 2: Adicionando Autenticação e Autorização
- Parte 3: Finalizando nosso CRUD de usuários
- Parte 4: Adicionando logs à nossa aplicação
- Parte 5: Enviando emails de confirmação e recuperação de senha
- Parte 6: Escrevendo testes
- Parte 7: Deploy!
Requisitos
O código da etapa anterior se encontra em um repositório no meu GitHub:
TL;DR
O código desta etapa também se encontra em um repositório no meu GitHub:
Envio de emails
O envio de emails é outro requisito sempre presente em qualquer aplicação, seja para enviar emails de confirmação de cadastro, emails de recuperação de senha, enviar relatório via email, entre diversas outras funcionalidades.
Há duas formas principais para se enviar emails através de sua aplicação. A primeira é delegar a tarefa de enviar emails para serviços especializados nisso como mailgun ou sendgrid. A outra alternativa é enviar emails através de seu próprio servidor, utilizando bibliotecas como sendmail.
Como a maioria dos serviços externos são pagos (normalmente com faixas de uso gratuitas para desenvolvimento), vamos explorar como enviar emails do nosso próprio servidor.
Mailer Module
Para enviarmos emails a partir da nossa aplicação vamos instalar um novo pacote:
$ npm install --save @nestjs-modules/mailer
O Mailer Module nada mais é do que o nodemailer encapsulado em um módulo do NestJS para facilitar seu uso junto com o framework.
Ao ler a documentação do Mailer Module vemos que para utilizar o serviço de envio de emails precisamos adicioná-lo aos imports do nosso app.module.ts. Entretanto, para inicializar o módulo, ele necessita de diversos parâmetros de configuração. Vamos primeiro criar esses parâmetros dentro da nossa pasta configs, no arquivo mailer.config.ts:
Alguns detalhes importantes:
- Já aproveitei a criação do arquivo de configuração para utilizar a ferramenta de templates handlebars para criação de nossos emails utilizando HTML e CSS. Isso deixa nossos emails mais amigáveis para o usuário final. Isso implica na criação da pasta templates na raiz de nosso projeto.
- Lembre-se de substituir na URL de transport seus dados para acesso a um serviço de SMTP, como o do Gmail:
smtps://alguem@gmail.com:senha-super-secreta@smtp.gmail.com
Vamos agora registrar nosso módulo de envio de emails em app.module.ts:
Agora, dentro da pasta templates, podemos criar as templates de nossos dois emails:
Repare que fizemos uso da ferramenta {{ }} para passarmos valores dinâmicos para dentro de nossos emails (no caso o token tanto de confirmação quanto de recuperação de senha). Para mais informações sobre as possibilidades de uso da ferramenta handlebars, recomendo uma leitura da documentação.
Enviando email de confirmação de cadastro
O email de confirmação é normalmente utilizado para validar a existência do endereço de email que o usuário utilizou durante o cadastro no sistema. Dentro de auth.service.ts vamos alterar o método de signUp para enviarmos o email de confirmação após a criação do usuário:
Realizamos duas alterações:
- Adicionamos no constructor o MailerService;
- Adicionamos no método signUp() o código para envio de emails.
- Repare que informamos o contexto do email, passando nele o valor da variável “token” que utilizamos no template do nosso email.
Vamos agora testar com o Postman, criando um novo usuário:
Agora, para que a confirmação de email funcione, precisamos criar um endpoint em nossa API que receba o token enviado por email, encontre o usuário ao qual ele pertence e confirme seu endereço de email.
Vamos criar o método confirmEmail() no nosso auth.service.ts:
Repare que para mostrarmos que o endereço de email do usuário foi confirmado basta mudarmos o valor de “confirmationToken” para null, o que indica para nossa aplicação que o usuário validou seu email. Para fazermos isso utilizamos o método update() do repositório. Esse método já vem pronto quando construímos um repositório do TypeORM e recebe dois parâmetros:
- Primeiro parâmetro, o critério para encontrar o registro no banco de dados que deve ser atualizado;
- Segundo parâmetro, um objeto com as alterações que devem ser feitas (no nosso caso, apenas removendo o conformationToken e colocando null no lugar).
Vamos agora chamar esse método a partir do auth.controller.ts:
Agora, para testar se está funcionando, podemos pegar o token do usuário no banco de dados (ou extraí-lo da URL que foi enviada por email) e enviá-lo ao endpoint:
Podemos conferir no banco de dados e veremos que, de fato, o usuário agora possui confirmationToken = null.
Email de recuperação de senha
Vamos analisar como funciona o fluxo de recuperação de senha:
- Usuário clica em “esqueci minha senha” no front-end;
- Usuário é encaminhado para um formulário onde informa seu endereço de email;
- O front-end envia esse endereço de email para o back-end, que valida se existe um usuário com esse email e então envia um email com um token para o usuário recuperar sua senha;
- Usuário abre seu email e clica no link com o token e é encaminhado para uma tela do front com campos de senha e confirmação de senha;
- Front-end envia a senha e a confirmação em conjunto com o token extraído da URL para o back-end;
- Back-end encontra o usuário a partir do token informado e altera sua senha.
A princípio pode parecer bastante trabalho. Mas quando analisamos a tarefa da API nesse fluxo podemos reparar que temos que criar apenas dois endpoints:
- O primeiro recebe o endereço de email, valida se existe um usuário cadastrado com ele, gera um token de recuperação de senha, salva no banco de dados, e então dispara um email com o token de recuperação de senha.
- O segundo recebe o token, a senha e a confirmação de senha, encontra o usuário a partir do token, e altera sua senha.
Então vamos criar nosso primeiro endpoint. Vamos adicionar o método sendRecoverPasswordEmail() ao nosso auth.service.ts:
Agora vamos adicionar o endpoint que chama esse método a partir do auth.controller.ts:
Agora podemos realizar um teste no Postman:
Email recebido:
Agora precisamos construir nosso endpoint que receberá o token e a senha/confirmação de senha e realizará a alteração. Primeiro, vamos criar nosso DTO que irá abrigar a senha e a confirmação de senha, dentro da pasta auth/dto:
Como já trabalhamos com a geração dos hashs da senha no users.repository.ts faz sentido movermos a lógica para alteração de senha para lá:
Agora temos tudo pronto para criar os métodos necessários para alteração de senha dentro de auth.service.ts:
E por último, chamar o método a partir de auth.controller.ts:
Vamos testar nosso endpoint com o token que recebemos via email anteriormente (ou olhar o campo recoverToken no banco de dados):
Agora vamos testar o login com nossa nova senha:
Bônus
Eis que já temos praticamente tudo pronto no nosso código para adicionar uma funcionalidade muito importante à nossa aplicação: Permitir que o usuário altere sua senha sem passar por todo o processo de recuperação, caso ele já esteja autenticado e queira apenas mudar sua senha atual.
Para isso serão necessárias apenas duas alterações. A primeira, uma alteração muito pequena: Dentro do nosso jwt.strategy.ts (lembram-se dele? foi criado na parte 2 dessa série!) quando buscamos nosso usuário pelo id do token, vamos agora incluir o id do usuário no select de campos a serem retornados do banco. Vou explicar o motivo disso já, já.
Agora vamos adicionar no auth.controller.ts um endpoint que recebe a senha e confirmação de senha e um ID de usuário realizar a alteração:
E é isso. Está pronta a funcionalidade de alteração de senha. Precisávamos alterar o JwtStrategy para retornar o ID do usuário para que pudéssemos comparar o ID enviado como parâmetro de URL na requisição com o ID do usuário autenticado no momento. Reparem que segundo a nossa regra de autorização, um Administrador tem permissão para alterar a senha de qualquer usuário. Esse tipo de autorização varia, é claro, de projeto para projeto. Como já havíamos criado no auth.service.ts um método que recebe o DTO com senha e confirmação de senha junto com um ID de usuário e realiza a alteração de senha, só precisamos chamar esse método para o nosso endpoint funcionar.