Nesta aula, você aprenderá como criar um endpoint de login para a sua aplicação usando o Fastify. O objetivo é fornecer aos usuários a capacidade de fazer login na sua aplicação e obter um token de acesso válido para acessar outros recursos da API.
Este é um material auxiliar do bootcamp CrazyStack Node.js do DevDoido. Ele servirá como uma espécie de documentação de alguns códigos vistos durante as aulas apenas como material complementar, garanta já sua vaga no bootcamp clicando AQUI!.
Para isso, você utilizará o conceito de controller e algumas boas práticas de programação, como validação de dados de entrada, tratamento de erros e geração de tokens. Além disso, você implementará o uso de camadas de abstração, que permitirão a separação das responsabilidades e uma melhor organização do código.
Ao final da aula, você terá uma compreensão clara de como implementar um endpoint de login usando o Fastify e terá a base necessária para criar outros endpoints para a sua aplicação.
const bodyLoginJsonSchema = {
type: "object",
required: ["email", "password", "passwordConfirmation"],
properties: {
email: { type: "string" },
password: { type: "string" },
passwordConfirmation: { type: "string" },
},
};
const loginResponse = {
200: {
type: "object",
properties: {
refreshToken: { type: "string" },
accessToken: { type: "string" },
user: {
type: "object",
properties: {
_id: { type: "string" },
email: { type: "string" },
name: { type: "string" },
role: { type: "string" },
active: { type: "boolean" },
coord: {
type: "object",
properties: {
type: { type: "string", enum: ["Point"] },
coordinates: { type: "array", items: { type: "number" } },
},
},
},
},
},
},
};
export const loginPostSchema = {
schema: {
body: bodyLoginJsonSchema,
response: loginResponse,
},
};
Este código é uma definição do formato esperado para uma requisição HTTP POST
para o endpoint de login, e o formato da resposta esperada para esse endpoint.
A primeira constante, bodyLoginJsonSchema
, define o esquema JSON para o corpo da requisição, ou seja, o formato do objeto que será enviado com a requisição HTTP. Ele especifica que é esperado um objeto, com três propriedades obrigatórias: email
, password
, e passwordConfirmation
. O tipo de cada uma dessas propriedades é especificado como sendo uma string.
A segunda constante, loginResponse
, define o formato do objeto de resposta para o endpoint de login. É especificado que o status HTTP da resposta será 200 (OK), e que o corpo da resposta será um objeto com três propriedades: refreshToken
, accessToken
e user
. A propriedade user
é um objeto com informações do usuário, incluindo sua ID, e-mail, nome, papel, se está ativo, e sua localização (representada como um objeto "Point" com coordenadas).
Por fim, a constante loginPostSchema
é uma definição de rota que combina as especificações de corpo de requisição e resposta em um único objeto. Esse objeto será usado para validar a requisição e a resposta, para garantir que estejam no formato esperado.
import {
Authentication,
HttpRequest,
HttpResponse,
Validation,
badRequest,
forbidden,
unauthorized,
addDays,
ok,
} from "@/application/helpers";
import { Controller } from "@/application/infra/contracts";
import { LoadUser } from "@/slices/user/useCases";
import { AddAccount } from "@/slices/account/useCases";
import { EmailInUseError } from "@/application/errors";
export class LoginController extends Controller {
constructor(
private readonly validation: Validation,
private readonly loadUser: LoadUser,
private readonly authentication: Authentication,
private readonly addAccount: AddAccount
) {
super();
}
async execute(httpRequest: HttpRequest<any>): Promise<HttpResponse<any>> {
const errors = this.validation.validate(httpRequest?.body);
if (errors?.length > 0) {
return badRequest(errors);
}
const { email, password } = httpRequest?.body;
const userExists = await this.loadUser({
fields: { email },
options: { projection: { password: 0 } },
});
if (!userExists) {
return forbidden(new EmailInUseError());
}
delete httpRequest?.body?.passwordConfirmation;
const { accessToken = null, refreshToken = null } =
(await this.authentication.auth(email, password)) || {};
if (!accessToken || !refreshToken) {
return unauthorized();
}
await this.addAccount({
createdById: userExists?._id as string,
name: userExists?.name as string,
refreshToken,
active: true,
expiresAt: addDays(new Date(), 1) as unknown as string,
});
return ok({ user: userExists, accessToken, refreshToken });
}
}
Este é o código de um controlador de login na aplicação. O controlador é responsável por gerenciar o processo de login de um usuário.
O construtor do controlador recebe quatro dependências:
- A validação: responsável por validar os dados recebidos na requisição.
- LoadUser: uma classe de caso de uso responsável por carregar um usuário com base em seu email.
- A autenticação: responsável por autenticar o usuário com base nas informações fornecidas.
- Adicionar Conta: uma classe de caso de uso responsável por adicionar uma nova conta para o usuário.
O método execute() é responsável por processar a requisição de login. Ele começa verificando se há erros na validação dos dados da requisição. Se houver erros, ele retorna uma resposta HTTP com o código 400 (Bad Request) e a lista de erros.
Em seguida, ele usa o LoadUser para carregar o usuário com base no email fornecido na requisição. Se o usuário não existir, ele retorna uma resposta HTTP com o código 403 (Forbidden) e uma mensagem de erro.
Se o usuário existir, ele usa o objeto de autenticação para autenticar o usuário com base nas informações de email e senha fornecidas na requisição. Se a autenticação falhar, ele retorna uma resposta HTTP com o código 401 (Unauthorized).
Se a autenticação for bem-sucedida, ele usa a classe Adicionar Conta para adicionar uma nova conta para o usuário, com o token de atualização e a data de expiração. Finalmente, ele retorna uma resposta HTTP com o código 200 (OK) e os dados do usuário, incluindo o token de acesso e o token de atualização.
import { makeLogController } from "@/application/decorators/logControllerFactory";
import { makeDbAuthentication, makeValidationComposite } from "@/application/factories";
import { Controller } from "@/application/infra/contracts";
import { makeAddAccountFactory } from "@/slices/account/useCases";
import { LoginController } from "@/slices/user/controllers";
import { makeLoadUserFactory } from "@/slices/user/useCases";
export const makeLoginController = (): Controller => {
const requiredFields = ["email", "password", "passwordConfirmation"];
return makeLogController(
"login",
new LoginController(
makeValidationComposite(requiredFields),
makeLoadUserFactory(),
makeDbAuthentication(),
makeAddAccountFactory()
)
);
};
Esse código define uma função que cria um controlador de login para a aplicação. A função makeLoginController
retorna um objeto que implementa a interface Controller
com os métodos necessários para realizar uma ação.
O controlador é criado a partir da função makeLogController
, que aceita como argumentos uma string para identificar o tipo de ação (neste caso, "login") e uma instância de LoginController
.
A instância de LoginController
é criada a partir de vários componentes que são criados usando outras funções de fábrica, como makeValidationComposite
, makeLoadUserFactory
, makeDbAuthentication
e makeAddAccountFactory
. Estas funções de fábrica são responsáveis por criar instâncias dos componentes necessários para a autenticação e validação dos dados do usuário.
Os campos obrigatórios que são necessários para realizar a ação de login são especificados como uma array de strings no início da função makeLoginController
, na variável requiredFields
.
Testes de integração
import { makeFastifyInstance } from "@/index";
import { Collection } from "mongodb";
import { MongoHelper } from "@/application/infra";
import { hash } from "bcrypt";
jest.setTimeout(50000);
let userCollection: Collection;
const userBody = {
email: "gustavoteste41@hotmail.com",
name: "Gustavo",
role: "client",
password: "123456",
passwordConfirmation: "123456",
coord: { type: "Point", coordinates: [-46.693419, -23.568704] },
};
describe("Route api/auth", () => {
let fastify: any;
beforeAll(async () => {
const client = await MongoHelper.connect(process.env.MONGO_URL as string);
fastify = await makeFastifyInstance(client);
await fastify.listen({ port: 3000, host: "0.0.0.0" });
});
afterAll(async () => {
await fastify.close();
await MongoHelper.disconnect();
fastify = null;
});
beforeEach(async () => {
userCollection = await MongoHelper.getCollection("user");
await userCollection.deleteMany({});
});
describe("POST /api/auth/signup", () => {
test("Should return 200 on signup", async () => {
const response = await fastify.inject({
method: "POST",
url: "/api/auth/signup",
payload: userBody,
});
const responseBody = JSON.parse(response.body);
expect(response.statusCode).toBe(200);
expect(responseBody.user).toBeTruthy();
expect(responseBody.accessToken).toBeTruthy();
expect(responseBody.refreshToken).toBeTruthy();
});
test("Should return 403 if email is already in use", async () => {
await userCollection.insertOne(userBody);
const response = await fastify.inject({
method: "POST",
url: "/api/auth/signup",
payload: userBody,
});
const responseBody = JSON.parse(response.body);
expect(response.statusCode).toBe(403);
expect(responseBody).toEqual({
error: "Forbidden",
statusCode: 403,
message: "The received email is already in use",
});
});
test("Should return 400 if password and passwordConfirmation are different", async () => {
const response = await fastify.inject({
method: "POST",
url: "/api/auth/signup",
payload: {
...userBody,
passwordConfirmation: "1234567",
},
});
const responseBody = JSON.parse(response.body);
expect(response.statusCode).toBe(400);
expect(responseBody).toEqual([
{ mensagem: "Invalid param: passwordConfirmation", name: "InvalidParamError" },
]);
});
test("Should return 400 if email is invalid", async () => {
const response = await fastify.inject({
method: "POST",
url: "/api/auth/signup",
payload: {
...userBody,
email: "gustavoteste41hotmail.com",
},
});
const responseBody = JSON.parse(response.body);
expect(response.statusCode).toBe(400);
expect(responseBody).toEqual([
{ mensagem: "Invalid param: email", name: "InvalidParamError" },
]);
});
});
describe("POST /api/auth/login", () => {
test("Should return 403 on login if user does not exists", async () => {
const response = await fastify.inject({
method: "POST",
url: "/api/auth/login",
payload: userBody,
});
const responseBody = JSON.parse(response.body);
expect(response.statusCode).toBe(403);
expect(responseBody).toEqual({
error: "Forbidden",
statusCode: 403,
message: "The received email is already in use",
});
});
test("Should return 200 if user exists and password is correct", async () => {
const password = await hash(userBody.password, 12);
await userCollection.insertOne({ ...userBody, password });
const response = await fastify.inject({
method: "POST",
url: "/api/auth/login",
payload: userBody,
});
const responseBody = JSON.parse(response.body);
expect(response.statusCode).toBe(200);
expect(responseBody.user).toBeTruthy();
expect(responseBody.accessToken).toBeTruthy();
expect(responseBody.refreshToken).toBeTruthy();
});
test("Should return 400 if password is different", async () => {
const password = await hash(userBody.password, 12);
await userCollection.insertOne({ ...userBody, password });
const response = await fastify.inject({
method: "POST",
url: "/api/auth/login",
payload: {
...userBody,
passwordConfirmation: "1234567",
password: "1234567",
},
});
const responseBody = JSON.parse(response.body);
expect(response.statusCode).toBe(401);
expect(responseBody).toEqual({
error: "Unauthorized",
statusCode: 401,
message: "Unauthorized",
});
});
test("Should return 400 if email is invalid", async () => {
const response = await fastify.inject({
method: "POST",
url: "/api/auth/login",
payload: {
...userBody,
email: "gustavoteste41hotmail.com",
},
});
const responseBody = JSON.parse(response.body);
expect(response.statusCode).toBe(400);
expect(responseBody).toEqual([
{ mensagem: "Invalid param: email", name: "InvalidParamError" },
]);
});
});
});
Este é um conjunto de testes para uma API REST construída usando o framework Fastify e o MongoDB para armazenar dados de usuários. A API tem dois endpoints para autenticação: POST /api/auth/signup e POST /api/auth/login.
O endpoint de registro permite que os usuários criem uma conta enviando uma carga JSON com seu email, nome, senha, confirmação de senha e coordenadas. A API retornará um código de status 200 e uma resposta JSON com o usuário criado e dois tokens (acesso e atualização) se a carga for válida e o email não estiver sendo usado. Se o email já estiver sendo usado, a API retornará um código de status 403 e uma mensagem de erro JSON. Se a senha e a confirmação de senha não corresponderem, ou se o email for inválido, a API retornará um código de status 400 e uma mensagem de erro JSON.
O endpoint de login permite que os usuários façam login enviando uma carga JSON com seu email e senha. A API retornará um código de status 200 e uma resposta JSON com o usuário logado e dois tokens se o email existir e a senha estiver correta. Se o email não existir ou se a senha estiver incorreta, a API retornará um código de status 403 e uma mensagem de erro JSON.
O conjunto de testes usa Jest para testar e faz uso do método inject do Fastify para simular solicitações HTTP à API. Ele se conecta a uma instância do MongoDB antes de cada teste e se desconecta depois de cada teste. O conjunto de testes também tem um tempo limite de 50 segundos.