- Introdução
- Configurações iniciais
- Servidor 🖥️
- Rotas 🗺️
- Controllers 🕹️
- Base de dados 🗄️
- Middlewares
- Rotas 📍
API-authentication é um projeto desenvolvido em função da necessidade de se aplicar conhecimentos adquiridos em estudos da linguagem javascript no lado do servidor em projetos reais. Criado com diversos pacotes publicos disponiveis no NPM, esse projeto dispoe de diversas funcionalidades como sign-up, login e entre outras. Essa documentação foi criada no intuito de servir como uma anotação para consultas futuras, de modo que todo o conteúdo apresentado aqui será o mais completo e detalhado possível.
Neste projeto usaremos o modelo MVC(Model, viewers, controllers), onde a requisição chega ao servidor, é direcionada ao routes que analisa qual endpoint buscar (auth/users), direcionando a solicitação ao Middleware que vai fazer algum tipo de tratamento nos dados e chegar ao controller que é onde será respondida a solicitação. Esse fluxo pode ser visto no diagrama abaixo
Client Request ---> Server ---> Routes.js ---> Middleware ---> Controller ---> Response
pois podemos trabalhar com separação de responsabilidades:
- Routes: Determinam "para onde" a requisição vai com base no endpoint.
- Middleware: Adiciona camadas para pré-processamento (como autenticação ou validação).
- Controller: Contém a lógica de processamento de requisições.
- Response: Contém a resposta da requisições.
Vamos iniciar a configuração do nosso projeto com base no fluxo mostrado acima, ou seja, vamos criar os arquivos server Router e os demais de modo que as requisições fluem até o controller e então criaremos toda a lógica da resposta.
Para iniciar o projeto, no terminal do VS Code dentro do diretório da pasta onde vai criar os arquivo do projeto digite
npm init
Usamos esse commando para criar um package.json na nossa aplicação, que será o responsável por armazenar as dependencias da aplicação. Com o arquivo criado precisamos instalar os pacotes iniciais que vamos utilizar para criar nossa API. No terminal digite
npm i express mongoose nodemon --save-dev dotenv
Eles serão adicionados ao package.json como dependencias. Na raiz do projeto deve-se criar o arquivo server.js que é onde será adicionada toda a lógica por trás do servidor.
require(".dotenv").config(); // Carrega as variáveis de ambiente do arquivo .env para process.env
const express = require("express"); // Importa o framework Express para criar o servidor
const app = express(); // Cria uma instância do servidor com Express
// server config
const port = process.env.port || 5000; // Define a porta do servidor (process.env.port ou 5000)
app.get("/", (req, res) => {
res.send("Bem vindo a API"); // Configura a rota principal (/) para responder com "Bem vindo a API"
});
app.listen(port, () => {
console.log("Servidor on"); // Exibe mensagem indicando que o servidor está ativo
console.log(`Acesse em http://localhost:${port}`); // Informa a URL para acessar o servidor
});
Aqui vamos configurar para onde nosso servidor vai redirecionar as requisições e quais rotas o usuário vai poder acessar. Para isso crie um pasta nomeada de src (source) na raiz do projeto, dentro dela a pasta Routes, que terá os arquivos routes.js, users.js e auth.js como mostrado abaixo:
API-Authentication
|
|- node_modules 🗃️
|
|--src 🗃️
| |-routes 📁
| |- users.js 📄
| |- auth.js 📄
| |- routes.js 📄
|
|- package.json 📄
|- server.js 📄
no arquivo routes.js faremos:
const express = require("express"); // Importa o módulo 'express' para criar o roteador
const router = express.Router(); // Cria um novo roteador usando o express.Router()
// Controllers das rotas
const users = require("./users"); // Importa o arquivo 'users.js' para lidar com as rotas de usuários
const auth = require("./auth"); // Importa o arquivo 'task.js' para lidar com as rotas de tarefas
// Rotas para endpoint Users
router.use("/users", users); // Define que as rotas com '/users' serão tratadas pelo controlador 'users'
router.use("/auth", auth); // Define que as rotas com '/tasks' serão tratadas pelo controlador 'tasks'
module.exports = router; // Exporta o roteador para que ele possa ser usado em outros arquivos
nos arquivos users e auth que são importados acima, adicionaremos as rotas publicas e privadas do nosso sistema de autenticação, pode ser visto abaixo:
const route = express.Router();
// Controllers
const authController = require("../controllers/authController");
// Routes
route.post("/signup", authController.signUp); // Rota para criar uma nova conta (signup)
route.post("/login", authController.signIn); // Rota para fazer login
route.post("/logout", authController.logout); // Rota para fazer logout (remover o token)
route.post("/logoutAll", authController.logoutAll); // Rota para fazer logout de todos os dispositivos
module.exports = route;
const route = express.Router(); // Cria um novo roteador usando o express.Router()
// Controllers
const userController = require("../controllers/usersControllers"); // Importa o controlador de usuários, onde as funções de lógica estão.
// Routes
route.get("/profile", userController.getusers); // Rota para obter o perfil de usuário.
route.patch("/profile", userController.patchUser); // Rota para atualizar os dados do usuário.
route.delete("/profile", userController.deleteUser); // Rota para excluir a conta do usuário.
module.exports = route; // Exporta o roteador para que ele possa ser utilizado em outros arquivos
A função de processamento de cada uma dessas rotas foram colocadas dentro de seu respectivo controller, como podemos ver nas importações
const authController = require("../controllers/authController");
const userController = require("../controllers/usersControllers");
é dentro desses controllers que iremos condensar todas as rotas que manipularão os dados e responderão os usuário.
Com todas as rotas criadas e configuradas, vamos agora na pasta controlers e criar os arquivos authController.js e usersControllers.js, de modo que a estrutura do nosso projeto até o momento seja:
API-Authentication
|
|- node_modules 🗃️
|
|--src 🗃️
| |-Controller 📁
| |- authController.js 📄
| |- usersController.js 📄
| |-routes 📁
| |- users.js 📄
| |- auth.js 📄
| |- index.js 📄
|
|- package.json 📄
|- server.js 📄
Dentro desses arquivos faremos:
exports.signUp = async (req, res) => {}; // Controlador responsável por criar um novo usuário
exports.signIn = async (req, res) => {}; // Controlador responsável pelo login do usuário
exports.logout = async (req, res) => {}; // Controlador responsável por realizar o logout do usuário
exports.logoutAll = async (req, res) => {}; // Controlador responsável por realizar o logout de todos os dispositivos
exports.getusers = async (req, res) => {}; // Rota para obter o perfil do usuário autenticado
exports.deleteUser = async (req, res) => {}; // Rota para excluir a conta do usuário
exports.patchUser = async (req, res) => {}; // Rota para atualizar as informações do usuário
exports.uploads = async (req, res) => {}; // Rota para realizar upload de foto de perfil
exports.deleteAvatar = async (req, res) => {}; // Rota para excluir a foto de perfil do usuário
exports.getAvatar = async (req, res) => {}; // Rota para obter a foto de perfil do usuário
Vamos adicionar a lógica necessaria em cada uma das rotas acima de modo decrescente.
Como vamos iniciar nossa API pela rota de signup, precisamos estabelecer a conexão com a base de dados, uma vez que na rota em questão precisamos salvar as credenciais do usuário e isso só é possivel se tivermos onde salvar os dados.
Um model em uma aplicação representa a estrutura e as regras de um dado armazenado no banco de dados. Ele define os campos, tipos de dados e validações necessários para criar e manipular esses dados. Além disso, o model permite interagir com o banco de dados, como realizar consultas, atualizações, exclusões e adições. É usado para centralizar a lógica de negócios relacionada às informações. Em geral, o model é uma peça do padrão MVC (Model-View-Controller).
Dentro da pasta src criamos uma terceira pasta chamada model e dentro dela um arquivo que nomearemos como userModel.js.
API-Authentication
|
|--node_modules 📁
|
|--src 🗃️
| |--Controller 📁
| |--routes 🛤️
| | |-- index.js 📄
| | |-- users.js 📄
| | |-- auth.js 📄
| |
| |--model 📂
| | |-- userModel.js 📄
|
|--package.json 📄
|--server.js 📄
Dentro de userModel.js faremos
// Todo pacote importado em atualizações no Model devem ser adicionados abaixo do mongoose
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
userName: {
type: String,
required: true, // O nome de usuário é obrigatório
trim: true, // Sanitiza o input
},
email: {
type: String,
required: true, // O e-mail é obrigatório
unique: true, // O e-mail deve ser único no banco de dados
trim: true, // Sanitiza o input
},
password: {
type: String,
required: true, // A senha é obrigatória
trim: true, // Sanitiza o input
},
});
// _______________________________
// Toda a lógica das atualização devem ser feitas entre as linhas na ordem mostrada abaixo
// .
// .
// 3° atualização
// 2° atualização
// 1° atualização
// _______________________________
// Definindo o modelo de dados "Users" baseado no esquema "userSchema"
const userData = mongoose.model("Users", userSchema);
module.exports = userData;
No model acima adicionamos algumas configurações que são importantes no inicio, e algumas outras serão adicionadas a medida que vamos desenvolvendo a aplicação. No model acima um usuário cadastrado na base de dados terá seus dados armazenados como:
{
"_id": "new ObjectId('213asf5554s5533525')",
"name": "Caio",
"email": "caio@gmail.com",
"password": "123456"
}
Antes de falarmos da senha e caracteristicas dos dados quando os salvamos na base de dados, precisamos primeiro ter onde salvar os dados, e sem a conexão com a base isso é impossivel.
Na pasta src vamos adicinar a pasta DB onde criamos o arquivo db.js, que será o responsavel por toda a logica da conexão com a base de dados
API-Authentication
│
├── src 🗃️
│ ├── Controller 📁
│ ├── Model 📂
│ │ └── userModel.js
│ ├── Routes 📁
│ │ ├── auth.js
│ │ ├── users.js
│ │ └── index.js
│ ├── db 🗄️
│ └── db.js
├── package.json 📄
├── server.js 📄
const mongoose = require("mongoose");
const Events = require("events");
const dbEvents = new Events();
mongoose
.connect(process.env.DB_URL)
.then(() => {
dbEvents.emit("connected");
})
.catch((error) => {
console.log(error);
});
module.exports = { dbEvents };
para poder acessar o process.env.DB_URL, precisamos na raiz no projeto criar um arquivo .env e um .gitignore. O .env é onde adicionamos as variaveis de ambiente, e o .gitignore e onde colocamos a extensão dos arquivos que não queremos que sejam enviados para o github.
API-Authentication
│
├── src 🗃️
│ ├── Controller 📁
│ ├── Model 📂
│ │ └── userModel.js
│ ├── Routes 📁
│ │ ├── auth.js
│ │ ├── users.js
│ │ └── index.js
│ ├── db 🗄️
│ │ └── db.js
├── .env 📄
├── .gitignore 📄
├── package.json 📄
├── server.js 📄
no arquivo .env e .gitignore adicionamos
DB_URL: mongodb://127.0.0.1/27017/auth
que é a string de conexão que o mongoose vai utilizar para conectar com a base de dados. Já no no .gitignore fazemos
node_modules
.env
Agora esses arquivos não serão mais mapeados para o github e não corremos o risco de expor dados sensiveis da nossa aplicação. Para finalizar a conexão com a base de dados, precisamos importar esse arquivo no arquivo server para que quando o servidor seja iniciado, a conexão seja estabelecida e o evento dbEvents.emit("connected")
seja disparado e então o servidor liberado.
server.js (Conexão com a base)
require(".dotenv").config();
const express = require("express");
const app = express();
const port = process.env.port || 5000;
// Conexão com a base de dados (1° atualizações)
const { dbEvents } = require("/src/db/dbConnection");
app.get("/", (req, res) => {
res.send("Bem vindo a API");
});
dbEvents.on("connected", () => {
app.listen(port, () => {
console.log("Servidor on");
console.log(`Acesse em http://localhost:${port}`);
});
});
no código
// Ouvindo o evento 'connected' do dbEvents, que é disparado quando a conexão com o banco de dados é bem-sucedida
dbEvents.on("connected", () => {
// Inicia o servidor na porta especificada, e exibe mensagens de sucesso
app.listen(port, () => {
console.log("Servidor on"); // Exibe mensagem indicando que o servidor está ativo
console.log(`Acesse em http://localhost:${port}`); // Informa a URL para acessar o servidor
});
});
indica que o servidor só será liberado após o sinal "connected" ser emitido. Feito a atualização acima, vc deve ser capaz de acessar o servidor já com a base conectada.
Com a base de dados configurada, já quase podemos iniciar as configurações das nossas rotas, porém nosso servidor ainda não é capaz de interpretar e responder requisições no formato JSON
e nem de receber objetos complexos no corpo da requisição. Para resolver isso teremos de atualizar nosso arquivo server.js.
require(".dotenv").config();
const express = require("express");
const app = express();
const port = process.env.port || 5000;
const { dbEvents } = require("/src/db/dbConnection");
// Middlewares (2° atualização)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get("/", (req, res) => {
res.send("Bem vindo a API");
});
dbEvents.on("connected", () => {
app.listen(port, () => {
console.log("Servidor on");
console.log(`Acesse em http://localhost:${port}`);
});
});
Agora podemos adicionar um primeiro usuário a base de dados e responde-lo usando JSON.
Um outro conceito que é muito importante para aprendermos antes de colocarmos a mão na massa, é o de middleware.
Um middleware é uma função que intercepta requisições (request) e respostas (response) no fluxo de uma aplicação, executando lógica antes de alcançar o manipulador final da rota. Ele pode ser usado para tarefas como autenticação, logging, manipulação de dados ou tratamento de erros. Middleware é aplicado globalmente ou em rotas específicas e funciona em uma sequência definida. No Express, usa-se app.use() ou diretamente na rota.
No nosso projeto teremos dois middlewares principais, o que será responsável por validar os dados enviados no corpo da requisição e um de verificação de token. Na pasta src crie:
📁 API-Authentication
├── 📁 node_modules 🗃️
│
├── 📁 src 🗃️
│ ├── 📁 controllers 📁
│ │ ├── authController.js 📄
│ │ ├── usersController.js 📄
│ │
│ ├── 📁 db 📁
│ │ └── db.js 📄
│ │
│ ├── 📁 middleware 📁
│ │ ├── userValidator.js 📄
│ │ └── verifyToken.js 📄
│ │
│ ├── 📁 model 📁
│ │ └── userModel.js 📄
│ │
│ ├── 📁 routes 📁
│ │ ├── users.js 📄
│ │ ├── auth.js 📄
│ │ └── index.js 📄
│
├── .env 📄
├── .gitignore 📄
├── package.json 📄
├── server.js 📄
Esse middleware será responsavel por validar os dados enviados no corpo da requisição. Para podermos escrever a lógica associada, vamos instalar o Validator. No terminal do VSCode digite
npm i validator
uma vez instalado, no arquivo userValidator.js
faremos
const validator = require('validator')
function validate(req, res, next) {
const {name. email, password} = req.body
if(!name || name.length <= 2) {
return res.status(400).json({message: 'O nome deve conter no minimo 3 caracteres'})
}
if(!email || validor.isEmail(email)) {
return res.status(400).json({message: 'Formato de email inválido'})
}
if(!password || validator.isLength(password, {min: 6})) {
return res.status(400).json({message: 'Senha deve conter no minimo 6 caracteres'})
}
next()
}
module.exports = validate
O middleware acima será adicionado no meio da rota de signup, como podemos ver abaixo:
routes.js:
const route = express.Router();
const authController = require("../controllers/authController");
const validator = require("../middlewares/userValidator");
// Middleware Validator
route.post("/signup", validator, authController.signUp);
route.post("/login", authController.signIn);
route.post("/logout", authController.logout);
route.post("/logoutAll", authController.logoutAll);
module.exports = route;
com os dados sendo validados. Vamos iniciar a implementação das nossas rotas.
Com as primeiras configurações feitas, podemos iniciar a lógica dentro dos controllers Controllers. Destaco que o codigo mostrads abaixo são basicos e que a medida em que formos adicionando novas funcionalidades, serão inclusas novas linhas de código.
Rota de sign-up é a rota que utilizamos para cadastrar um usuário ao nosso projeto, todavia, antes de se implementar essa rota na aplicação é necessário fazermos o hash da senha senha para que ela não fique em texto plano e gerar um token para o usuário nas rotas de signup e login, que é essencialmente uma forma de assinar o usuário digitalmente, ou seja, é uma forma de informar ao servidor que o usuário que está tentando fazer login na aplicação é realmente quem diz ser.
O hash é um processo de transformar uma senha em uma sequência única e fixa de caracteres, usando um algoritmo como bcrypt ou argon2. Ele é irreversível, ou seja, não é possível converter o hash de volta para a senha original. Quando o usuário tenta fazer login, a senha fornecida é novamente transformada em hash e comparada com o hash armazenado. Isso garante que a senha original nunca seja salva no banco de dados, aumentando a segurança. Além disso, técnicas como "salting" (adicionar valores aleatórios) tornam os hashes únicos, mesmo para senhas iguais. Um hash pode ser visto abaixo.
$2b$12$IHoTahYqFX3wPKLtvi.6/uM1xpIdcfZBYVgmvY2sMCepqY61aUkXe
Para proteger a senha dos usuários vamos precisar instalar argon2
npm i argon2
dentro do userModel.js adicione
// No topo do arquivo adicione
const argon2 = require('argon2')
// Função de hash da senha
userSchema.pre("save", (next) => {
const user = this;
try {
if (!user.isModified(password)) return next();
user.passoword = await argon2.sign(password, {
type: argon2.argon2id,
memoryCost: 2 ** 16,
timeCost: 5,
paralelism: 1,
})
next()
} catch(error) {
next(error)
}
});
// Toda atualização deve ser adicionada acima dessas diretivas
const userData = mongoose.model("Users", userSchema);
module.exports = userData;
Agora o que será salvo na base de dados é o hash da senha, e não a senha como texto plano. Limpe a base de dados e cadatre um novo usuário, o resultado deve ser algo como
{
"_id": "new ObjectId('213asf5554s5533525')",
"name": "Caio",
"email": "caio@gmail.com",
"password": "$2b$12$IHoTahYqFX3wPKLtvi.6/uM1xpIdcfZBYVgmvY2sMCepqY61aUkXe"
}
Uma vez que temos o usuário cadastrado na base de dados, podemos implementar a lógica que o permite utilizar os dados cadastrados para fazer login.
Um token é uma chave única usada para confirmar a identidade de um usuário ou aplicação. Ele é gerado ao fazer login e permite acessar recursos de forma segura. Tokens têm prazo de validade e podem ser cancelados, sendo muito usados em APIs e sistemas de autenticação.
Como utilizaremos a criação de tokens em mais de uma rota, iremos adiciona-lo ao model e atrela-lo aos documentos que forem criados pelo model, mas antes precisamos alterar o model, uma vez que queremos dar a oportunidade do usuário acessar sua conta em diversos dispositivos. No usermodel.js vamos adicionar um novo campo.
const userSchema = new mongoose.Schema({
userName: {
type: String,
required: true,
trim: true,
},
email: {
type: String,
required: true,
unique: true,
trim: true,
},
password: {
type: String,
required: true,
trim: true,
},
tokens: [
{
token: {
type: String,
required: true,
},
},
], // tokens será um array de objetos
});
. Para isso faremos:
No terminal instale o pacote:
npm i jsonwebtoken
Dentro do usermodel.js adicionamos:
// Inclua no top do model
const jwt = require("jsonwebtoken");
// Método gerar token
userSchema.methods.generateToken = async function () {
const user = this; // 'this' se refere ao usuário atual
// Gerando o token JWT com o ID do usuário, utilizando a chave secreta do ambiente
const userToken = jwt.sign({ _id: user._id }, process.env.JWT_SECRET, {
expiresIn: "7d", // O token vai expirar em 7 dias
});
// Verificando se o usuário já tem 4 tokens. Se sim, remove o mais antigo
if (user.tokens.length >= 4) {
return user.tokens?.shift();
}
// Se não tiver 4 tokens, adiciona o novo token à lista de tokens do usuário
user.tokens?.push({ userToken });
return userToken; // Retorna o token gerado
};
No metodo acima, usamos o process.env.JWT_SECRET
, que é a string que o json web token usa para assinar os tokens e poder decodifica-los posteriormente. Podemos criar essa chave de muitas formas difentes, mas é importante lembrar que ela deve ser uma senha muito forte, visto que é com essa senha que nosso servidor será capaz de garantir ou não acesso ao nosso site, e caso caia em mãos erradas, o atacante terá acesso total ao sistema. Recomendo que execute o resultado abaixo no terminal do VScode e use a saida como senha.
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
Para usar o valor gerado como senha do servidor, no arquivo .env adicionamos
JWT_SECRET = K8wHSvZHndOTPtQJYLYsx2DBOCL1n6DTEXQL8bGhb+U=
Agora temos uma senha segura que garante ao nosso servidor seja capaz de impedir tokens falsificados. Vamos utilizar todos os passos acima para garantir a segurança da nossa aplicação nas rotas que implementaremos.
Na rota signup faremos:
const UserModel = require("../model/userModel"); // Importando model
exports.signUp = async (req, res) => {
const { email, name, password } = req.body; // obtendo dados do formulário
try {
const existUser = await UserModel.findOne({ email });
if (existUser) {
throw new Error("Usuário já cadastrado");
}
const newUser = new UserModel({ name, email, password });
await newUser.generateToken();
try {
await newUser.save();
res.status(201).json({
message: "Usuário criado com sucesso",
newUser,
});
} catch (error) {
throw new Error(error.message);
}
} catch (error) {
res.status(401).json({ message: error.message });
}
};
const UserModel = require("../model/userModel");
const argon2 = require("argon2");
exports.login = async (req, res) => {
try {
const user = await UserModel.findByCredentials(req.body); // Vamos criar esse método no userModel
await user.generateToken();
return res.status(200).json({
success: true,
message: "Login realizado com sucesso.",
user,
});
} catch (error) {
return res.status(401).json({
success: false,
message: error.message,
user: null,
});
}
};
no userModel.js vamos adicionar o código mostrado abaixo
// Acima da função de fazer hash da senha adicione (2° atualização)
userSchema.statics.findByCredentials = async function ({ email, password }) {
const user = this;
const existUser = await User.findOne({ email });
if (!existUser) {
throw new Error("Usuário não cadastrado");
}
const isValidPassword = await argon2.verify(existUser.password, password);
if (!isValidPassword) {
throw new Error("Usuário não cadastrado");
}
return existUser;
};
O código acima anexamos um novo método ao model de modo que agora, podemos invocá-lo sempre que precisarmos verificar se o usuário está existe na base de dados.
A rota logout é a rota responsavel por finalizar a sessão do usuário na aplicação, para fazer precisamos primeiro verificar se o usuário que está tentando fazer o logout é autorizado a fazer isso, ou seja, se ele possui um token de acesso. Para isso antes de implementarmos a lógica de logout, devemos criar um middleware que verifica o token do usuário. Esse middleware será utilizado em todas as rotas privadas.
const jwt = require("jsonwebtoken");
const UserModel = require("../model/userModel");
async function verifyToken(req, res, next) {
try {
const token =
req.cookies.userToken ||
req.headers.authorization?.replace("Bearer ", "");
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await UserModel.findById(decoded._id);
if (!user) {
throw new Error("Usuário não cadastrado");
}
req.user = user;
req.token = token;
next();
} catch (error) {
res.status(401).json({ message: error.message });
}
}
exposts.logout = async function (req, res) {
try {
const user = await UserModel.findById();
} catch (error) {}
};