Projeto de Ecommerce Backend PARTE 2

Adaptações, Melhorias e Integrações com API 🤩

Developing

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

imagem 2023 11 04 132202970

essa tela de login ainda em fase de desenvolvimento 😅 mas ja tem bastante tratamento de erros como preenchimentos incorretos

DevTools / inspecionar elemento , console....

captura de tela 2023 11 04 132030

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:

imagem 2023 11 04 145630007

E-mail de código de verificação

codigoverificacao

E-mail de troca de senhas

imagem 2023 11 04 145850683

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)

imagem 2023 11 04 152542553

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, e url são colunas na tabela 'Uploads'.
  • beforeSave e beforeDestroy 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ário FormData.

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

Comentários