04 de novembro de 2023 • 31 min de leitura
Projeto de Ecommerce Backend PARTE 2
Adaptações, Melhorias e Integrações com API 🤩
Olá Pessoal , fiquei empolgado com a quantidade de likes e comentários no post que fiz sobre a parte 1 desse projeto de Ecommerce que resolvi dar continuidade no primeiro post que fiz aqui no blog explicando os passos do desenvolvimento.
O Primeiro post foi apenas um esboço , muitas correções adaptações e melhorias no código já foi adicionado.
Deixei o repositório do github privado por conta disso vou tentar fazer aqui um post bem detalhado
Dá muito trabalho esse projeto e decidi não deixar o código pra qualquer um simplesmente copiar e tomar pra si os créditos .
Dá trabalho também fazer esse post , e meu projeto não pode ficar parado kkk
Sem mais de longas e vamos ao que interessa 🚀
Nessa próxima fase de desenvolvimento iniciei implementando mais segurança , afinal ter apenas uma senha criptografada no banco de dados com o bcrypt foi só a primeira parte a próxima parte é Criar rotas de Administrador e ter as rotas monitoradas com Express-JWT , e integrações com serviços da aws como envio de e-mails com Amazon SES e upload de imagens para AWS S3 e armazenar imagens dos produtos no bucket s3.
Vamos dar uma olhada como está o package.json atualmente
{
"name": "ecommerce-backend",
"version": "1.5.5",
"description": "",
"main": "index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
},
"keywords": [],
"author": "FBS-DEV",
"license": "BSD 3-C",
"dependencies": {
"aws-sdk": "^2.1483.0",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"express": "^4.18.2",
"express-jwt": "^8.4.1",
"express-validator": "^7.0.1",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
"multer-s3": "^2.10.0",
"mysql2": "^3.6.1",
"sequelize": "^6.33.0"
},
"devDependencies": {
"dotenv": "^16.3.1",
"nodemon": "^3.0.1"
}
}
Express-jwt e jsonwebtoken
express-jwt
é um middleware para Express.js que fornece autenticação baseada em JSON Web Tokens (JWT) para aplicativos Node.js. Especificamente, o express-jwt
é usado para validar e decodificar tokens JWT presentes nos cabeçalhos HTTP das solicitações.
Alguns conceitos-chave associados ao express-jwt
:
- JSON Web Token (JWT): Um formato compacto e autônomo para representar informações entre partes como um objeto JSON. Esse token é frequentemente utilizado para autenticação e é composto por três seções separadas por pontos: o cabeçalho, a carga útil (dados) e a assinatura. Ele é normalmente enviado como um cabeçalho de autorização HTTP em solicitações para autenticar um usuário.
- Middleware de Autenticação: O
express-jwt
é um middleware que pode ser usado no Express.js para verificar se o token JWT fornecido está presente no cabeçalho da solicitação e, se presente, é válido e não expirou. - Decodificação do Token: Este middleware decodifica o token JWT e extrai as informações (como o payload) para autenticar ou autorizar uma requisição.
- Verificação de Assinatura e Validade: O
express-jwt
também é responsável por verificar a assinatura do token para garantir que ele não tenha sido modificado e está assinado corretamente com a chave secreta. Além disso, verifica a validade do token para garantir que não tenha expirado. - Gerenciamento de Rotas Protegidas: Usualmente, o
express-jwt
é usado em rotas específicas que requerem autenticação. Ele bloqueia o acesso a essas rotas se o token JWT fornecido for inválido ou ausente.
Usar o express-jwt
simplifica a implementação da autenticação baseada em tokens JWT em aplicativos Express.js, fornecendo uma maneira eficiente de proteger rotas, verificar a identidade do usuário e garantir que apenas usuários autenticados possam acessar recursos restritos.
Na pratica quando o Usuário fazer o login ele precisa ter esse token armazenado no sessionStorage do navegador
então vamos ver como isso funciona.
UserController.js / loginUser
const User = require("../models/User");
const jwt = require('jsonwebtoken');
const passwordUtils = require('../utils/passwordUtils'); // Importa funções úteis do bcrypt para lidar com senhas
const { Op } = require('sequelize'); // operadores no código da consulta em loginUser
exports.loginUser = async (req, res) => {
try {
const { email, username, password } = req.body;
const user = await User.findOne({
where: {
[Op.or]: [ // login com email ou username
{ email },
{ username }
]
}
});
if (!user) {
return res.status(404).json({ message: "Cliente não encontrado." });
}
const passwordMatch = await passwordUtils.comparePasswords(password, user.password);
if (passwordMatch) { // se a senha for correta faz o login
console.log(`🔓 Login realizado com sucesso para o usuário ${username} ${email} 🔓`);
const token = jwt.sign({ // gerar JWToken ao usuário
username: user.username,
isEmailValidated: user.isEmailValidated, // verifica se o usuario jav validou o email true or false pra notificar no front end
isAdmin: user.isAdmin, // adiciona no token o atributo isAdmin do usuario para verificar se é um adm quando as rotas forem executadas
isMod: user.isMod
},
process.env.JWT_SECRET, // senha do token definida no variável de ambiente
{
expiresIn: process.env.JWT_TIME // tempo de expiraçao do token definido na variável de ambiente
});
res.status(200).json({ message: `🔑 Login realizado Aproveite a Loja 🛒`, token });
} else {
res.status(400).json({ message: "⚠ Senha incorreta ⚠"});
}
} catch (error) {
console.error(error);
res.status(500).json({ message: error.message });
}
};
No trecho de código acima o corpo da requisição pode receber email , username e password , Utilizando o Método OP que é o operador de código de consulta do sequelize com [Op.or] é possível fazer o login com email ou com username se os dados baterem no preenchimento da tela de login.
Em seguida a função passwordMatch que já foi detalhada na primeira postagem do blog , usamos o método comparePasswords e comparar se foi digitada corretamente.
Se a senha está correta ' if (passwordMatch) ' entra a variável const token = jwt.sign . nessa parte é gerado o token e o token consegue armazenar atributos específicos , como o username , isEmailValidated , isAdmin ,isMod.
com esses dados no front end é possível definir rotas especificas para cada usuário ao validar essas condições pelo token .
As variáveis de ambiente definem o tempo de expiração do token e como a senha do token será definida
.env
JWT_SECRET=suaSenha@SuperSecret1
JWT_TIME=1h
Falando no modelo de User.js vamos ver como ele está atualmente.
User.js
const Sequelize = require('sequelize');
const db = require('../config/database');
const User = db.define('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
username: {
type: Sequelize.STRING(25), // limite de 25 caracteres
allowNull: false,
unique: true,
validate: {
len: [1, 25] // validar que o campo tem entre 1 e 25 caracteres
}
},
email: {
type: Sequelize.STRING(50),
allowNull: false,
unique: true,
validate: {
len: [1, 50]
}
},
password: {
type: Sequelize.STRING(60),
allowNull: false,
validate: {
len: [1, 60] // hashedpassword usa 60 caracteres
}
},
isAdmin: {
type: Sequelize.BOOLEAN,
defaultValue: false, // valor padrão é false (cliente)
},
isMod: {
type: Sequelize.BOOLEAN,
defaultValue: false, // valor padrão é false (cliente)
},
isEmailValidated: {
type: Sequelize.BOOLEAN,
defaultValue: false, // valor padrão é false
},
verificationCode: {
type: Sequelize.STRING,
allowNull: true,
},
});
// removido funções de db.sync() não é recomendado quando em produção , para isso deve se usar os recursos de migração do sequelize
module.exports = User;
isAdmin, isMod,isEmailValidated utilizam o tipo sequelize boolean , ou seja podem ser true ou false , por padrão todo usuário tem o tipo como falso, por isso ao inicializar o projeto é criado uma conta de administrador padrão que tem o atributo isAdmin , true , com isso é possível cadastrar produtos e utilizar rotas que serão exclusivas de administrador , o administrador poderá ter um controlador que ira definir cargos para usuários padrões como se um usuário poderá ser moderador isMod , ou definir outro administrador.
Depois irei explicar sobre o verificationCode.
Vamos dar uma olhada como está atualmente o arquivo Routes.js
Routes.js
const express = require('express');
const routes = express.Router();
const { expressjwt: ejwt } = require("express-jwt"); // Middleware
const Admincheck = require('./middlewares/Admincheck');
const Modcheck = require('./middlewares/Modcheck');
const UserController = require('./controllers/UserController'); // importando controladores
const ProductController = require('./controllers/ProductController');
const PasswordController = require('./controllers/PasswordController');
const EmailController = require('./controllers/EmailController');
const ProfileController = require('./controllers/ProfileController');
const AddressController = require('./controllers/AdressController');
const CategoryController = require('./controllers/CategoryController')
const OrderProductsController = require('./controllers/OrderProductsController');
const OrderController = require('./controllers/OrderController')
const AdminController = require('./controllers/AdminController')
const PaymentController = require('./controllers/PaymentController');
const UploadsController = require('./controllers/UploadsController')
// Middleware para autenticar o usuário usando o token gerado pelo jsonwebtoken
routes.use(
ejwt({ secret: process.env.JWT_SECRET, algorithms: ['HS256'] }).unless({ // desabilita a autenticaçao para as rotas login e signup pois nessas não é possivel ter o token , o token so é gerado apos o login.
path: ['/users/login', '/users/signup', '/public/products', '/public/users', '/email/code', '/email/verifyEmail', '/email/requestNewPassword']
})
);
//define as rotas de usuários.
routes.post('/users/signup', UserController.createUser);
routes.post('/users/login', UserController.loginUser);
routes.post('/public/users', UserController.getUserByEmail); // esqueceu seu nome de usuario ? digite seu email para buscar
routes.post('/email/code', EmailController.requestVerification);
routes.post('/email/verifyEmail', EmailController.verifyEmail);
routes.post('/email/requestNewPassword', EmailController.requestNewPassword);
// todas as outras rotas abaixo são monitoradas por expressjwt
routes.put('/email/update', EmailController.updateUserEmail);
// Define as rotas relacionadas aos produtos.
routes.get('/public/products', ProductController.getAllProducts); // rota sem token para o front end
routes.get('/products', ProductController.getAllProducts);
routes.get('/products/:id', ProductController.getProductById);
// define a rota de senhas.
routes.put('/password', PasswordController.updateUserPassword);
// define rotas de perfil
//routes.get('/profiles/:id', ProfileController.getProfilebyId);
routes.post('/profiles', ProfileController.createProfile);
routes.get('/profiles', ProfileController.getProfilebyUsername);
routes.put('/profiles', ProfileController.updateProfilebyUsername);
// define rotas de endereço
routes.post('/address/', AddressController.createAddress);
routes.get('/address/:id', AddressController.getAddressesByUserId);
routes.put('/address/:id', AddressController.updateAddress);
routes.delete('/address/:id', AddressController.deleteAddress);
//define rotas de categorias
routes.get('/categories', CategoryController.getAllCategories);
routes.get('/categories/:id', CategoryController.getCategoryById);
// rotas de formas de pagamentos
routes.post('/pagamentos', PaymentController.createPaymentMethod);
routes.get('/pagamentos/:id', PaymentController.getPaymentMethodsByUserId);
routes.put('/pagamentos/:id', PaymentController.updatePaymentMethod);
routes.delete('/pagamentos/:id', PaymentController.deletePaymentMethod);
// rotas de ordens de compra
routes.post('/ordens', OrderController.createOrder);
routes.get('/ordens/:id', OrderController.getOrderById);
routes.put('/ordens/:id', OrderController.updateOrder);
// Rotas para adicionar produtos a ordens de compra
routes.post('/addpedidos', OrderProductsController.addProductToOrder);
routes.put('/addpedidos/:id', OrderProductsController.removeProductFromOrder);
// Rota de upload de imagens
const multer = require('multer');
const multerConfig = require('./config/Multer');
routes.get('/uploads', UploadsController.getImages);
routes.post('/uploads', multer(multerConfig).single('file'), UploadsController.uploadImage);
routes.delete('/uploads/:id', UploadsController.deleteImage);
// Rotas de administrador
routes.use('/admin', Admincheck); // toda rota admin vai chamar o midleware de Autenticação de adm
routes.put('/admin/users/:id', AdminController.setRoles); // Atualizar um Usuário para adm ou moderador.
// Rotas de produtos para administrador
routes.post('/admin/products',multer(multerConfig).single('image'), ProductController.createProduct);
routes.put('/admin/products/:id', ProductController.updateProductById);
routes.delete('/admin/products/:id', ProductController.deleteProductById);
// Rotas de Usuários para administrador
routes.get('/admin/users', UserController.getAllUsers);
routes.get('/admin/users', UserController.getUsername);
routes.delete('/admin/users/', UserController.deleteUser);
// Rotas de Perfil para administrador
routes.get('/admin/profiles', ProfileController.getAllProfiles);
routes.delete('/admin/profiles/:id', ProfileController.deleteProfileById);
// Rotas de categorias para administrador
routes.post('/admin/categories', CategoryController.createCategory);
routes.put('/admin/categories/:id', CategoryController.updateCategory);
routes.delete('/admin/categories/:id', CategoryController.deleteCategory);
// Rotas de Ordens de compra para administrador
routes.get('/admin/ordens', OrderController.getAllOrders);
// Rotas de Moderador
routes.use('/mod', Modcheck);
routes.post('/mod/products', ProductController.createProduct);
routes.put('/mod/products/:id', ProductController.updateProductById);
routes.get('/mod/users', UserController.getAllUsers);
routes.get('/mod/users/:id', UserController.getUsername);
routes.get('/mod/profiles', ProfileController.getAllProfiles);
routes.delete('/mod/profiles/:id', ProfileController.deleteProfileById);
routes.get('/mod/ordens', OrderController.getAllOrders);
routes.post('/mod/categories', CategoryController.createCategory);
routes.put('/mod/categories/:id', CategoryController.updateCategory);
module.exports = routes;
Agora vamos ver na pratica como ocorre o login do usuário.
Usando Insomnia , POST : localhost:3000/users/login
{
"username": "admin",
"email": "",
"password": "admin"
}
O corpo da requisição pode preencher o username ou email ou até mesmo os 2 ao mesmo tempo no caso do insomnia
Resposta : 200 ok
{
"message": "🔑 Login realizado Aproveite a Loja 🛒",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNFbWFpbFZhbGlkYXRlZCI6ZmFsc2UsImlzQWRtaW4iOnRydWUsImlzTW9kIjpmYWxzZSwiaWF0IjoxNjk5MTEzMjQ3LCJleHAiOjE2OTkxMTY4NDd9.tJGGAb4y463oX4pAU4ictt5nvKGfB5LKH6Xs3WNqw9U"
}
Veja que na resposta o token veio no json isso acontece porque no trecho do código ' res.status(200).json({ message: 🔑 Login realizado Aproveite a Loja 🛒
, token }); ' o token está sendo passado , com isso o token pode ser armazenado no sessionStorage do front end. mas para ser armazenado no sessionStorage o Front end precisa estar configurado corretamente.
Vamos dar uma olhada como está o front end, afinal de contas nessa etapa do processo já estou fazendo as 2 coisas ao mesmo tempo tanto o front end quanto o backend assim eu vou adaptando os 2 conforme o código evolui.
O front-end utiliza o Framework React , sobre todos os passos iniciais do desenvolvimento do front end eu não pretendo fazer um post sobre mas apenas mostrando os trechos de códigos que interagem com o back end.
Para que o token seja armazenado no sessionStorage do navegador é necessário ter um midleware no projeto de front end no caso criei um arquivo chamado Api.js que vai conectar no backend e fazer as requisições utilizando o Axios '
Axios fornece uma API simples e flexível para realizar chamadas de API assíncronas, como obter dados de uma API RESTful, enviar dados para um servidor ou atualizar dados em tempo real .
Além disso no Front-end criei o component chamado Login.js que ficou muito legal pois nele tive que fazer muitas adaptações para por enviar a requisição corretamente com o payload no mesmo formato que feito no insomnia , pois no código utilizo as funções identifier e ele precisa ter o payload ajustado
Login.js Front-end
import React, { useState } from 'react';
import api from '../../api'; // Importa o arquivo de configuração do Axios
const Login = () => {
const [identifier, setIdentifier] = useState(''); // Armazena email ou username
const [password, setPassword] = useState('');
const [loginResponse, setloginResponse] = useState(null);
const [isSuccess, setIsSuccess] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
try {
// Construção do objeto de envio considerando a possibilidade de email ou username
const payload = { // strings vazias por padrão
username: '',
email: '',
password
};
if (identifier.includes('@')) {
payload.email = identifier; // Define email se o 'identifier' incluir '@'
} else {
payload.username = identifier; // Define username caso contrário
}
const response = await api.post('/users/login', payload); // faz o post igual utilizando o insomnia
const { token } = response.data; // armazena o token se resposta ok do backend
if (token) { // se recebeu token armazena no sessionStorage no formato token Bearer
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
sessionStorage.setItem('token', token);
setloginResponse('Login bem-sucedido! Redirecionando para a página inicial.');
setIsSuccess(true); // setIsSuccess true ou falso vai definer a cor da mensagem na linha 56
setTimeout(() => { // redireciona para home page apos 3 segundos
window.location.replace('/');
}, 3000);
}
} catch (error) {
const errorMessage = error.response?.data?.message; // retorna mensagens que o backend responde
setloginResponse(errorMessage);
setIsSuccess(false);
console.error('Erro ao fazer login Usuario ou senha invalidos:', error);
setTimeout(() => {
setloginResponse(null);
}, 5000);
}
};
return (
<div className="login-form">
<h2 className="txtcenter">🛒Acessar🤩</h2>
{loginResponse && <p style={{ color: isSuccess ? 'green' : 'red' }}>{loginResponse}</p>}
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Usuário ou E-mail"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
/>
<input
type="password"
placeholder="Senha"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
<p style={{ textAlign: 'center' }}>
<a href="/signup">Não possui Cadastro?</a>.
</p>
<p style={{ textAlign: 'center' }}>
<a href="/forgotpassword">Esqueceu sua Senha?</a>
</p>
<p style={{ textAlign: 'center' }}>
<a href="/forgotusername">Esqueceu seu Usuário?</a>
</p>
</div>
);
};
export default Login;
com identifier é possível preencher os campos de login com username ou senha , ele identifica que o payload será preenchido com email se houver um ' @ ' no preenchimento
ao ler os comentários no código acho que ficou fácil entender o processo , do token no trecho da linha 31 a 33
Api.js
import axios from 'axios';
const api = axios.create({
baseURL: process.env.REACT_APP_API_URL, // endereço do backend
});
api.interceptors.request.use((config) => {
const token = sessionStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response && error.response.status === 401) {
// Deslogar usuário automaticamente em caso de erro 401
sessionStorage.removeItem('token');
// Redirecionar para a página inicial
window.location.replace('/login');
}
return Promise.reject(error);
}
);
export default api;
Api.js é o responsável por se conectar ao backend e na linha 8 ele pega o token do sessionStorage e com isso verifica se o Usuário é um adm, mod ou se o usuário já validou email, etc , com isso no front end podemos criar links exclusivos para cada tipo de usuário
vamos ver no navegador como verificar o token no sessionStorage
Fazendo Login.
essa tela de login ainda em fase de desenvolvimento 😅 mas ja tem bastante tratamento de erros como preenchimentos incorretos
DevTools / inspecionar elemento , console....
No console da ferramenta de desenvolvedor do seu navegador digitar sessionStorage exibe o token , além disso podemos ver os atributos que defini anteriormente para verificar se o usuário é administrador ou se tem o e-mail validado
Agora já que mostrei a tela de login vou mostrar as outras telas como a de cadastro e como o front end deve garantir que as requisições sejam enviadas corretamente para o backend e evitar requisições desnecessárias ou invalidas , e o mais importante agora vemos a primeira integração com api externa , o envio de emails com Amazon SES.
por isso temos o aws-sdk no package.json , é o pacote de ferramenta de desenvolvimentos da AWS.
Amazon Simple Email Service
O Amazon SES é um provedor de serviços de e-mail baseado em nuvem que pode ser integrado a qualquer aplicação para automação de e-mails de alto volume.
UserController.js - Back-end
const User = require("../models/User"); // Importa o modelo do usuário
const passwordUtils = require('../utils/passwordUtils'); // Importa funções úteis do bcrypt para lidar com senhas
const generateVerificationCode = require('../utils/VerificationCode'); // importa funções úteis para gerar codigos // Importa a biblioteca de geração de tokens JWT
const EmailController = require('../controllers/EmailController'); // Importa as funções de envio de email
// operadores no código da consulta em loginUser
exports.createUser = async (req, res) => { // Rota para criar um novo usuário
try {
const { username, email, password } = req.body; // Obtém os dados do corpo da requisição
const userAlreadyExists = await User.findOne({ where: { email } }); // Verifica se o usuário já existe no banco de dados
if (userAlreadyExists) {
res.status(400).json({ message: `⚠ O Cliente com e-mail ${email} já está cadastrado ⚠`}); // Retorna uma resposta com erro 400 se o usuário já existir
}
const hashedPassword = await passwordUtils.hashPassword(password); // Gera um hash da senha com a função hashPassword do bcrypt
const verificationCode = generateVerificationCode(8); // gera o godigo de verificação
await EmailController.sendWelcome(email, verificationCode); // envia o email de bem vindo com o codigo para validar o email
const user = await User.create({ username, email, password: hashedPassword, verificationCode }); // Cadastra o usuário no banco de dados com a senha criptografada
console.log(user);
res.status(201).json({ message: `🤖 O Cliente ${username} com E-mail. ${email}, foi Cadastrado com Sucesso! 🤖` });
} catch (error) {
console.error(error);
res.status(400).json({ message: "⚠ E-mail inválido ou já cadastrado ⚠" });
}
};
o método para criar o usuário exporta algumas funções , a primeira já foi dito anteriormente na primeira postagem no blog , o passwordUtils, mas agora temos o generateVerificationCode e temos um novo controlador o EmailController.js que exporta para o controlador UserController.js o método sendWelcome.
lendo os comentários no código vemos oque cada um faz mas para entender melhor vamos agora ao EmailController.js e ver como ele se integra ao Amazon SES.
EmailController.js - Back-end
const AWS = require('aws-sdk');
// Configurar as credenciais da AWS, região e serviço SES
const ses = new AWS.SES({
apiVersion: '2010-12-01',
accessKeyId: process.env.AWS_ACCESS_KEY_ID, // Substitua pelas suas credenciais de acesso
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_DEFAULT_REGION, // Substitua por sua região, por exemplo, 'us-east-1'
});
// Função para enviar e-mail de verificação
exports.sendVerificationEmail = (email, verificationCode) => {
const params = {
Destination: {
ToAddresses: [email],
},
Message: {
Body: {
Text: {
Data: `Seu código de verificação é: ${verificationCode}`,
},
},
Subject: {
Data: 'Código de Verificação',
},
},
Source: process.env.EMAIL_SENDER, // Substitua pelo seu e-mail de envio
};
return ses.sendEmail(params).promise();
};
const sendNewPassword = (email, newpassword) => { // não é exportado para outros controladores
const params = {
Destination: {
ToAddresses: [email],
},
Message: {
Body: {
Text: {
Data: `Sua nova senha é: \n ${newpassword} \n Troque sua senha após o Login`,
},
},
Subject: {
Data: 'Nova senha Temporária ',
},
},
Source: process.env.EMAIL_SENDER,
};
return ses.sendEmail(params).promise();
};
exports.sendWelcome = (email, verificationCode) => {
const link = `${process.env.APP_URL}/profile`; // Defina a variável link conforme necessário
const params = {
Destination: {
ToAddresses: [email],
},
Message: {
Body: {
Text: {
Data: `Bem vindo ao nosso comércio eletrônico. \n Não esqueça de validar seu e-mail \n Seu código de verificação é: \n ${verificationCode} \n Utilize esse código no Menu de perfil: ${link}`,
},
},
Subject: {
Data: 'Cadastro Realizado com Sucesso na Loja Virtual',
},
},
Source: process.env.EMAIL_SENDER,
};
return ses.sendEmail(params).promise();
};
Nesse trecho de código vamos que inicialmente já foi criado as variáveis de ambiente com os nomes AWS_ACCESS_KEY_ID , AWS_SECRET_ACCESS_KEY ,AWS_DEFAULT_REGION que já devem estar corretamente definidas no arquivo .env
vemos também como é a estrutura do código para enviar o e-mail , cada função corresponde para uma mensagem de e-mail diferente, exports.sendVerificationEmail é uma solicitação de envio de código de verificação de e-mail caso o usuário ainda não tenha feito a verificação quando recebeu ou perdeu o e-mail de boas vindas exports.sendWelcome que também envia um código de verificação , depois temos o const sendNewPassword que é o método que o usuário vai solicitar uma nova senha , esse método está em outro trecho do código do controlador de Email , por isso ele não é exportado .
mas antes de continuar vamos ver como é feito a geração do código de verificação ou de senha provisória.
Utils/VerificationCode.js - Back-end
function generateVerificationCode(length) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
let code = '';
for (let i = 0; i < length; i++) {
code += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return code;
}
module.exports = generateVerificationCode;
nessa função javascript é gerado uma combinação de números e letras e quando é chamado a função em algum controlador ' const verificationCode = generateVerificationCode(8); ' podemos definir quantos caracteres queremos , para o código de verificação defini 8 caracteres , para a senha provisória troquei para (16)
abaixo vemos como é feito uma solicitação de troca de senhas.
exports.requestNewPassword = async (req, res) => {
try {
const { verificationCode, email } = req.body; // corpo da requisição deve ter um verification code
const user = await User.findOne({ where: { email, verificationCode } }); // procura no db na tabela User.js o email fornecido e o codigo de verificação
if (!user) {
return res.status(404).json({ message: 'Código de verificação inválido.' }); // erro 404 caso nao encontrar email ou codigo invalido
}
const newpassword = generateVerificationCode(16); // Gera um código de 16 caracteres para a nova senha
const hashedPassword = await passwordUtils.hashPassword(newpassword);
await User.update({ password: hashedPassword }, { where: { email } }); // salva a nova senha criptografada no db / o usuario deve trocar de senha e ter a senha criptografada
await sendNewPassword(email, newpassword); // Envio do e-mail de senha provisória de 16 caracteres
return res.status(200).json({ message: 'Email com nova senha enviado!' });
} catch (error) {
console.error(error);
return res.status(500).json({ message: 'Ocorreu um erro ao solicitar a nova senha.' });
}
};
usamos generateVerificationCode(16) e esse código é enviado para o Cliente , mas no banco de dados ele ja é salvo de forma criptografada com ' hashedPassword '
agora vamos ver como chega os e-mails
E-mail de boas vindas:
E-mail de código de verificação
E-mail de troca de senhas
Agora como está ficando meio longo o post vamos ver como é feita a integração com AWS S3 e como é feito o upload de imagens para adicionar imagens aos produtos.
Primeira parte é instalar o pacote multer e multer-s3
Multer:
O Multer é um middleware Node.js que simplifica o upload de arquivos. Ele é comumente usado com o framework Express para lidar com o recebimento e armazenamento de arquivos enviados por formulários em aplicativos da web. O Multer oferece suporte para armazenamento local, mas também pode ser configurado para enviar arquivos para serviços de armazenamento em nuvem, como Amazon S3.
Multer-S3:
O Multer-S3 é uma extensão do Multer projetada especificamente para enviar arquivos para o Amazon S3, o serviço de armazenamento de objetos da Amazon Web Services (AWS). Ele facilita o upload de arquivos diretamente para o Amazon S3, sem a necessidade de armazenamento local, o que pode ser útil em aplicativos que se baseiam na AWS para armazenamento de arquivos. Este pacote permite o envio direto dos arquivos do usuário para um bucket do S3.
config/Multer.js - Back-end
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
const aws = require('aws-sdk');
const multerS3 = require('multer-s3');
const storageTypes = {
local: multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.resolve(__dirname, '..', '..', 'tmp', 'uploads'));
},
filename: (req, file, cb) => {
crypto.randomBytes(16, (err, hash) => {
if (err) return cb(err);
const fileExtension = path.extname(file.originalname);
const fileName = `${hash.toString('hex')}-${fileExtension}`;
const key = `${hash.toString('hex')}-${fileExtension}`; // fix localstorage sem key no db
file.key = key; // Adicionei essa linha para definir a chave no objeto do arquivo.
cb(null, fileName);
});
},
}),
s3: multerS3({
s3: new aws.S3(),
bucket: process.env.BUCKET_NAME,
contentType: multerS3.AUTO_CONTENT_TYPE,
acl: 'public-read',
key: (req, file, cb) => {
crypto.randomBytes(16, (err, hash) => { // logica melhorada comparada ao projeto fbsdevuploads
if (err) return cb(err);
const fileExtension = path.extname(file.originalname); // path.extname para obter a extensão do arquivo e garantir que somente a extensão seja mantida no novo nome do arquivo.
const fileName = `${hash.toString('hex')}-${fileExtension}`; // tranforma uma hash hexadecimal em string e adiciona a extensão da imagem , ex : hex.jpg
cb(null, fileName);
});
},
}),
};
const multerConfig = {
dest: path.resolve(__dirname, '..', '..', 'tmp', 'uploads'),
storage: storageTypes[process.env.STORAGE_TYPE],
limits: {
fileSize: 1024 * 1024 * 8, // 8MB
},
fileFilter: (req, file, cb) => {
const allowedMimes = [
'image/jpeg',
'image/png',
'image/jpg',
'image/bmp',
'image/gif',
'image/jfif',
];
if (allowedMimes.includes(file.mimetype)) {
cb(null, true); // if allowedMimes 200 ok
} else {
cb(new Error('Formato de arquivo inválido.')); // verificação de erros nas funções de callback do crypto.randomBytes
}
},
};
module.exports = multerConfig;
esse código segue o mesmo padrão do código que fiz em um dos meus primeiros projetos de uploads com aws s3 que pode ser acessado no Repositório do GitHub que inclusive está online pela Amazon Elastic Bean Stalk
só que essa nova versão o código está melhorado escrito de uma forma melhor.
a configuração do código acima define 2 formas de salvar uma imagem , no inicio usa a função multer.diskstorage que define que as imagens podem ser salva localmente na pasta do projeto através de uma pasta na raiz do projeto de nome tmp/uploads
a variável const fileExtension com o método path.extname separa do nome original da imagem o seu formato , exemplo jpeg,png,bmp,gif e jfif.
const fileName usa o método ${hash.toString('hex')} que é basicamente uma formula para gerar um código hexadecimal e -${fileExtension}`; é a extensão do arquivo que foi recuperado na variável anterior.
dessa maneira os arquivos de imagem devem ser salvos da seguinte maneira ,
exemplo : ' a26e8f5ae9408c4f86cc2b9f6fddb052-.jpg '
exemplo utilizando o meu projeto de uploads já online com o link completo ' https://fbsdevuploads.s3.amazonaws.com/a26e8f5ae9408c4f86cc2b9f6fddb052-.jpg '
Em meu projeto de uploads o banco de dados utilizados é o mongodb e como o projeto só serve para fazer upload de imagem para o s3 e criar uma url publica foi mais simples do que o processo que é feito agora , pois agora temos que atribuir imagens aos produtos então vamos ver primeiramente como as tabelas estão se relacionando no banco de dados pelo diagrama de entidades e relacionamento ou
Enhanced Entity-Relationship (EER)
Na imagem vemos que o Modelo Uploads.js não se relaciona com products.js no inicio tive dificuldades em criar uma logica funcional para que ao fazer upload a url da imagem que era salva no uploads.js fosse exportada para products.js via chave estrangeira , mas das formas que tentei não foi possível até eu encontrar uma solução.. mas antes vamos ver como está o modelo Uploads.js e Products.js para entender melhor...
Uploads.js - Back-end
const Sequelize = require('sequelize');
const aws = require('aws-sdk');
const fs = require('fs').promises;
const path = require('path');
const s3 = new aws.S3();
const db = require('../config/database');
const Uploads = db.define('Uploads', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: Sequelize.STRING
},
size: {
type: Sequelize.INTEGER
},
key: {
type: Sequelize.STRING
},
url: {
type: Sequelize.STRING(255)
}
});
Uploads.beforeSave(async (upload) => {
if (!upload.url) {
try {
upload.url = `${process.env.APP_URL}/files/${upload.key}`;
await upload.save();
} catch (error) {
console.error(error);
}
}
});
Uploads.beforeDestroy(async (upload) => {
if (process.env.STORAGE_TYPE === 's3') { // se o storage type for s3 vai deletar do bucket s3 , se nao vai remover do storage_type=local
try {
await s3.deleteObject({
Bucket: process.env.BUCKET_NAME,
Key: upload.key
}).promise();
} catch (error) {
console.error(error);
}
} else {
try { // se for desenvolviemento local deleta o arquivo localmente
await fs.unlink(
path.resolve(__dirname, '..', '..', 'tmp', 'uploads', upload.key)
);
} catch (error) {
console.error(error);
}
}
});
module.exports = Uploads;
para entender melhor o modelo de uploads vamos olhar para o controlador de produtos na criação de produtos.
ProductController.js - Back-end
const Product = require('../models/Product');
const Uploads = require('../models/Uploads');
createProduct: async (req, res) => {
try {
const { productName, price, description, categoryId, quantity } = req.body;
const productExists = await Product.findOne({ where: { productName } });
if (!productExists) { // se não houver produto cadastrado com o mesmo nome então execute o codigo abaixo e cadastre.
let imageUrl;
if (req.file) {
// Se um arquivo foi carregado, armazene a chave no produto
const { key } = await Uploads.create({
name: req.file.originalname,
size: req.file.size,
key: req.file.key,
url: req.file.location,
});
imageUrl = key;
}
const newProduct = new Product({
productName,
price,
description,
categoryId,
quantity: quantity || 1,
image_key: imageUrl, // Definir a chave da imagem no produto
});
const savedProduct = await newProduct.save();
console.log(savedProduct);
res.status(201).json(savedProduct);
} else {
return res.status(400).json({ message: "Este produto já está cadastrado" });
}
} catch (error) {
res.status(400).json({ error: "Não foi possível cadastrar o Produto, verifique a Categoria" });
}
},
Product.js
const Sequelize = require('sequelize');
const db = require('../config/database');
const Category = require('../models/Category');
const Product = db.define('products', {
productId: {
type: Sequelize.INTEGER,
primaryKey: true,
unique: true,
autoIncrement: true
},
productName: {
type: Sequelize.STRING,
allowNull: true,
unique: true,
},
price: {
type: Sequelize.FLOAT,
allowNull: false
},
description: {
type: Sequelize.TEXT,
allowNull: true
},
quantity: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
},
image_key: {
type: Sequelize.STRING, // Armazene a chave (key) da imagem
allowNull: true,
},
categoryId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: Category,
key: 'id'
}
}
});
Product.belongsTo(Category, { foreignKey: 'categoryId' });
module.exports = Product;
Resumindo
Uploads.js
Neste arquivo, está definido um modelo para uploads, principalmente para imagens. A tabela 'Uploads' possui colunas como 'id', 'name', 'size', 'key' e 'url' para armazenar as informações sobre os arquivos enviados.
id
,name
,size
,key
, eurl
são colunas na tabela 'Uploads'.beforeSave
ebeforeDestroy
são gatilhos que são acionados antes de salvar e excluir um upload, respectivamente.beforeSave
é responsável por atribuir uma URL para os uploads com base na chave (key
) do arquivo.beforeDestroy
verifica o tipo de armazenamento (local ou S3) e, caso seja armazenamento no S3, deleta o arquivo do bucket. Se for armazenamento local, ele deleta o arquivo do sistema de arquivos local.
Product.js
o modelo de produtos agora possui o atributo image_key , onde no front end essa key vai virar uma url.
e se relaciona com a tabela de categorias , o produto só pode ser criado se houver uma categoria cadastrada.
ProductController.js
Este controlador lida com operações relacionadas aos produtos, como criar, listar, atualizar e excluir produtos. A função createProduct
é responsável por criar um produto. Nela, se um arquivo for carregado, a chave (key
) do arquivo é armazenada no modelo 'Uploads'. Além disso, o image_key
do produto é definido como a chave do arquivo.
Multer.js
Neste arquivo, as configurações do Multer são definidas com base no tipo de armazenamento (local ou AWS S3). A função fileFilter
define os tipos MIME permitidos para os arquivos a serem carregados. As configurações são especificadas para o armazenamento local ou no S3, incluindo o local para armazenar os arquivos, limites de tamanho, filtros de arquivos e outras opções.
Destaque nos termos de armazenamento de arquivos:
image_key
: Utilizado no modelo de produtos para associar a chave do arquivo de upload ao produto.file.key
: É uma propriedade criada e associada ao arquivo no Multer, utilizada para identificar o arquivo na aplicação.
A associação entre o modelo 'Uploads' e o modelo 'Product' é feita através do uso da chave (key
) do arquivo. A chave do arquivo é armazenada na tabela 'Uploads' e referenciada na tabela 'Product' para identificar a imagem associada a um determinado produto.
na tabela de produtos é salvo apenas a key da imagem e não uma url inteira igual é salvo na tabela de uploads
então no front end adaptei isso, vamos ver como ficou o código para transformar as imagem em url e todo o processo de upload como é feito por lá
Abaixo, vou explicar o fluxo para listar os produtos, a exibição de imagens e o envio de dados de produtos com imagens:
Home.js
import React, { useState, useEffect } from 'react';
import api from '../../api';
import '../../App.css';
const Home = () => {
const [products, setProducts] = useState([]);
useEffect(() => {
const fetchProducts = async () => {
try {
const response = await api.get('/public/products');
setProducts(response.data);
} catch (error) {
console.error('Erro ao buscar os produtos:', error);
}
};
fetchProducts();
}, []);
return (
<div className="container">
<h1>Produtos</h1>
<ul>
{products.map((product, index) => (
<li key={index}>
<div>
<p>Nome: {product.productName}</p>
<p>Preço: R$ {product.price}</p>
{product.image_key && (
<img
src={`${process.env.REACT_APP_AWS_S3_URL}${product.image_key}`}
alt={`Imagem de ${product.productName}`}
style={{ width: '200px', height: '200px' }}
/>
)}
</div>
</li>
))}
</ul>
</div>
);
};
export default Home;
src={${process.env.REACT_APP_AWS_S3_URL}${product.image_key}
} aqui pegamos a url do buckets3 através de variável de ambiente e concatenamos com a key que foi salva na tabela do produto quando o produto é criado
Home.js é responsável por exibir uma lista de produtos, incluindo suas imagens.
Vamos destacar a parte relevante:
Exibição de Produtos e Imagens:
- Utiliza um loop sobre a lista de produtos (
products.map
) para criar elementos<li>
para cada produto. - Para cada produto, exibe informações como nome e preço.
- Verifica se
product.image_key
existe para exibir a imagem do produto. - Se a
image_key
existe, é utilizada para construir o URL da imagem, que é renderizado no<img>
.
vejamos como o produto é criado no front end
CreateProduct.js
import React, { useState, useEffect } from 'react';
import api from '../../api';
import '../../App.css';
const CreateProduct = () => {
// Define o estado para os dados do formulário
const [formData, setFormData] = useState({
productName: '',
price: '',
description: '',
image: null,
categoryId: '',
});
// Define o estado para armazenar as categorias
const [categories, setCategories] = useState([]);
// Busca as categorias disponíveis ao carregar a página
useEffect(() => {
const fetchCategories = async () => {
try {
const response = await api.get('/categories');
setCategories(response.data);
} catch (error) {
console.error('Erro ao buscar as categorias:', error);
}
};
fetchCategories();
}, []);
// Atualiza o estado dos campos do formulário ao serem preenchidos
const handleChange = (e) => {
const { name, value, files } = e.target;
// Se o campo alterado for o de imagem, atualiza o estado com o arquivo selecionado
if (name === 'image') {
setFormData({
...formData,
image: files[0],
});
} else {
// Atualiza o estado com os outros campos do formulário
setFormData({
...formData,
[name]: value,
});
}
};
// Envia os dados do formulário para criar um novo produto
const handleSubmit = async (e) => {
e.preventDefault();
// Cria um objeto FormData para armazenar os dados do formulário
const productData = new FormData();
productData.append('productName', formData.productName);
productData.append('price', formData.price);
productData.append('description', formData.description);
productData.append('categoryId', formData.categoryId);
productData.append('image', formData.image);
try {
// Faz uma requisição para criar um novo produto enviando o FormData
const response = await api.post('/admin/products', productData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
console.log('Produto cadastrado com sucesso:', response.data);
// Aqui seria possível redirecionar o usuário ou executar outra ação desejada
} catch (error) {
console.error('Erro ao cadastrar o produto:', error);
}
};
return (
<div className="container">
<h1>Cadastrar Produto</h1>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="productName">Nome do Produto</label>
<input
type="text"
name="productName"
value={formData.productName}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label htmlFor="price">Preço</label>
<input
type="text"
name="price"
value={formData.price}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label htmlFor="description">Descrição</label>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label htmlFor="categoryId">Categoria</label>
<select
name="categoryId"
value={formData.categoryId}
onChange={handleChange}
>
<option value="">Selecione uma categoria</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.categoryName}
</option>
))}
</select>
</div>
<div className="form-group">
<label htmlFor="image">Imagem do Produto</label>
<input
type="file"
name="image"
onChange={handleChange}
/>
</div>
<button type="submit">Cadastrar Produto</button>
</form>
</div>
);
};
export default CreateProduct;
Neste arquivo, ocorre o envio de informações de um novo produto, incluindo a imagem do produto.
Envio de Dados de Produto:
- Formulário de cadastro de produtos onde são inseridos o nome, preço, descrição, categoria e a imagem do produto.
- O campo de imagem é do tipo
<input type="file">
para que o usuário possa selecionar um arquivo. - O evento
handleChange
é acionado quando há uma mudança nos campos do formulário. - Quando um arquivo de imagem é selecionado, é armazenado no estado
formData
. - No envio do formulário, é criado um objeto
FormData
para encapsular os dados, incluindo a imagem. - O envio é feito com
api.post
para a rota '/admin/products'. - A imagem é enviada com o campo
image
do formulárioFormData
.
O handleChange
é responsável por atualizar o estado formData
com as informações inseridas nos campos do formulário. Se o campo de imagem for preenchido, ele é adicionado ao objeto FormData
.
O fluxo de upload de imagens no front-end envolve o uso de um campo do tipo input
com o atributo type="file"
, o que permite selecionar um arquivo no dispositivo do usuário. Quando o formulário é enviado, os dados, incluindo a imagem selecionada, são enviados para a rota '/admin/products' usando api.post
. Lá, o back-end recebe os dados e armazena a imagem associada ao produto.
A associação entre a chave de uploads (image_key
) e a URL da imagem geralmente é feita no backend, onde a chave é relacionada a um URL por meio do armazenamento de arquivos no servidor. Por exemplo, se as imagens estiverem armazenadas em um servidor S3, o backend poderia gerar um URL baseado na chave do arquivo para acessar a imagem. Este URL gerado é então enviado para o frontend e utilizado para exibir a imagem.
Até aqui acho que já dei muito spoiler 😅 se você curtiu esse post da um like e comenta , quando eu terminar o projeto ai sim farei um vídeo e ai vai ficar fácil de entender todo o processo. ainda falta muita coisa para implementar esse post ficou longo isso porque nen detalhei muito todos os controladores que já foi criado e só mostrei um pouco dos códigos do front end e nem mostrei como é feito a parte de configurações lá no console da AWS , para isso quando o projeto estiver mais robusto ai farei um vídeo.
até o próximo post ou vídeo me acompanhe no linkedin