eusouregislima / devburgerAPI2.0

0 stars 0 forks source link

Parte 2 da estruturação da API #2

Open eusouregislima opened 5 months ago

eusouregislima commented 5 months ago

JWT - JSON web token (Segurança) -> Dividido em 3 partes

  1. Fala sobre o tipo do token
  2. Payload base64 -> Passa informações do usuário (id, nome ou email, soment o que precisar)
  3. Assinatura do token (Chave secreta)

-> Também pode ter uma data de expiração -> Quanto menos valer, mais seguro é, porém a usuabilidade do usuário é importante ser levada em conta.

Instalando o jwt -> npm install jsonwebtoken Vamos no Session Controller e vamos gerar o token quando o usuário fizer a autenticação.

O segundo parâmetro que o jwt espera é a terceira parte do token (assinatura) Utilizaremos o MD5 Hash Generator Coloco uma string aleatória, que no caso utilizei devburger-api-devregis e a string gerada eu passei como segundo parâmetro. E no terceiro parâmetro eu passo um objeto de configuração falando quando o token expira. No caso da hamburgueria colocamos '5d', isso pq não exige-se um nível de segurança muito absurdo, mas no caso de um banco isso poderia ser minutos.

Trazendo as configurações de autenticação para um arquivo exclusivo, no caso criamos em config um arquivo chamado auth.js com o conteúdo:

export default { secret: '51326cf0a09e70f58e1887dfc70305da', expiresIn: '5d' }

Importei esse arquivo no meu SessionController e passei da seguinte forma: token: jwt.sign({ id: user.id }, authConfig.secret, { expiresIn: authConfig.expiresIn, }

eusouregislima commented 5 months ago

Procedimentos para passar o token para a aplicação Temos os Headers (Cabeçalho) que podemos passar parâmetros Dentro do nosso client de teste de requisições (no caso estou usando o HTTPie, em headers adiciono o Authorization, e ele vai receber o nosso token, então nessa fase eu copio o token gerado na requisição se session e colo no Authorization criado com o valor -> Bearer + token

Revisando o fluxo da aplicação

Request -> MIDDLEWARE -> controller -> model -> database -> response O middleware intercepta a requisição antes de chegar no Controller (como se fosse a imigração para entrar em um país)

Criamos a pasta middleware com o arquivo auth.js, esse arquivo recebe uma funcção com 3 parâmetros, request, response e next. O Next é para prossegir com o fluxo. Exportamos essa função e importamos nas rotas, e passamos o middleare como primeiro parâmetro dentro da route de get/products.

A primeiro momento colocamos no console o request.headers.authorization para ver se o token estava chegando corretamente.

Agora ajeitando o fluxo real da aplicação

  1. Verificar se o token existe na requisição Nota: Outra forma de passar o token no HTTPie ao invés de ser pelo headers na mão é pelo Auth apenas colando o token.

Então pegamos o token do request.headers.authorization e verificamos se ele existe com um if, caso não exista, já retorna o erro

const authToken = request.headers.authorization;

if (!authToken) { return response.status(401).json({ error: 'Token not Provided' }); }

  1. Pegar o token Como o token vem inteiro com 3 partes, e eu não quero ele inteiro, vou usar o métido split(".") que pega uma string e cria um array separando pelo caractere solicitado no parâmetro, no caso o ponto final. Três formas de uso: const token = authToken.split(" ").at(1) -> Aqui separei pelo espaço removendo a palavra bearer me sobrando só o token (usamos essa) const token = authToken.split(" ")[1] -> Mesma coisa const [_, token]= authToken.split(" ") -> Aqui desestruturando e pegando somente a segunda posição do array.

  2. Verificar o Token Conforme a documentação do jwt colocamos um try catch passando no try o método verify com o token recebido no primeiro parâmetro, o secret importado de authConfig e uma funcão anônima com o err e o decoded. Se houver erro já estoura o catch, senão a primeiro momento colocamos o decoded no console e veio o id do usuário, a data de criação do token e de expiração.

Na sequência guardamos o id que vem do decoded na request da sequinte forma: request.userId = decoded.id; Para que o id fique disponível para toda a request adiante.

Assim ficou o arquivo import jwt from 'jsonwebtoken'; import authConfig from '../config/auth';

function authMiddleware(request, response, next) { const authToken = request.headers.authorization;

if (!authToken) { return response.status(401).json({ error: 'Token not Provided' }); }

const token = authToken.split(' ').at(1);

try { jwt.verify(token, authConfig.secret, (err, decoded) => { if (err) { throw new Error(); }

  request.userId = decoded.id;

  return next();
});

} catch (err) { return response.status(401).json({ error: 'Token is invalid' }); } }

export default authMiddleware;

E no final vamos ajeitar as rotas para que elas usem o middleware routes.use(authMiddleware);

Ao adicionar o routes.use todas as rotas que estiverem abaixo irão utilizar esse meddleware

eusouregislima commented 5 months ago

A próxima aula explicou sobre relacionamento de tabelas que pode ser 1:1 1:N N:N

Na próxima aula vamos criar a tabela de categories yarn sequelize migration:create --name create-categories-table

Criamos essa tabela com id, name (sendo único), created e updated. Rodamos a Migrate -> yarn sequelize db:migrate O Container precisa estar rodando

Na sequência criamos a model e o controller do mesmo jeito que o de products O próximo passo é colocar essa rota em ação

Nunca esquecer de passar o model no index do database

eusouregislima commented 5 months ago

Na próxima aula validamos se havia categoria repetida e colocamos para retornar no create somente o nome e o id.

Trecho de código const categoryExists = await Category.findOne({ where: { name, }, });

if (categoryExists) {
  return response.status(400).json({ error: 'Category already exists' });
}

const { id } = await Category.create({
  name,
});
eusouregislima commented 5 months ago

Criando e excluindo dados de uma tabela

Nota: o método down sempre tem o inverso do método up

para rodar a migration usamos o comando yarn sequelize db:migrate Para rodar o método down usamos o comando yarn sequelize db:migrate:undo

eusouregislima commented 5 months ago

Agora estamos criando a migration que faz o relacinamento entre tabelas. Aqui é importante ter o total entendimento do que está acontecendo.

Antes na tabela de produtos eu tinha a categoria adicionada, que era uma string. Porém agora eu tenho uma tabela de categorias adicionada com id e nome. A ideia é que na tabela de produtos tenha um category_id com esse id da tabela de categorias apenas referenciando.

Então o método up está adicionando uma coluna em products chamada category_id que é um integer pq o id na tabela de categorias também é, recebe o atributo references com o model categories e a key: id que quer dizer que faz referencia a tabela de categorias no atributo id, isso dentro de um objeto conforme a migration add-category-id-column Também recebe o atributo -> onUpdate: 'CASCADE' que diz que cada vez que houver mudança, ele mudará aqui também. E -> onDelete: 'SET NULL', para quando algo for deletado, ele deixe como null. E o allowNull: true.

Assim ficou o método up completo:

async up(queryInterface, Sequelize) { await queryInterface.addColumn('products', 'categoty_id', { type: Sequelize.INTEGER, references: { model: 'categories', key: 'id', }, onUpdate: 'CASCADE', onDelete: 'SET NULL', allowNull: true, }); },

eusouregislima commented 5 months ago

Avisando na model sobre o relacionamento de tabelas

Adicionamos o return this que estava faltando

e adicionamos um static dessa forma:

static associate(models){ this.belongsTo(models.Category) }

Significa que esse model pertence ao model de categoria

O método completo ficou assim

static associate(models){ this.belongsTo(models.Category, { foreignKey: 'category_id', as: 'category' }) }

E que dizer que o category_id é uma chave estrangeira como -> category.

Na sequencia fomos lá no arquivo index do database avisar que existem associações que se existirem precisam ser consideradas.

Assim ficou a estrutura do arquivo na parte do init:

init() { this.connection = new Sequelize(configDatabase); models .map((model) => model.init(this.connection)) .map( (model) => model.associate && model.associate(this.connection.models), ); }

eusouregislima commented 5 months ago

Fazendo as alterações no controller para funcionar o relacionamento

Alteramos o category para category_id em todos os locais e ele deixou de ser uma string para ser um number.

E para que ao criar um novo produto ele não me retorne apenas category_id: 1, e ele me retorne o id e nome da categoria fizemos essas mudanças no product controller na rota de findAll

async index(request, response) { const products = await Product.findAll({ include: [ { model: Category, as: 'category', attributes: ['id', 'name'], }, ], });

eusouregislima commented 5 months ago

Um pouco sobre banco de dados SQL e NoSQL O MongoDB é a opção de banco de dados NoSQL que vamos utilizar para controlar os pedidos. O banco de dados relacional tem a característica mais estruturada, e o banco de dados não relacional é bem menos estruturado, aceitando qualquer coisa praticamente. Quando não se conhece bem os dados, ou ele tende a mudar muito, o NoSQL é o recomendado. Já quando se conhece a estrutura dos dados e sabe-se que eles serão robustos vale usar o SQL.

Características MongoDB - SQL Database ---------------- Database Collection --------------- Table Document --------------- Row Field --------------------- Column Aggregation -------------- Joins

eusouregislima commented 5 months ago

Criando um novo conteiner com o Mongo docker run --name devburger-mongo -p 27017:270 17 -d -t mongo

Como o sequelize não conversa com o Mongo, precisaremos instalar o mongoose npm install mongoose

Importamos o mongoose no index do database para estabelecer a conexão na class Database adicionamos this.mongo()

E abrimos uma conexão mongo() { this.mongoConnection = mongoose.connect( 'mongodb://localhost:27017/devburger', ); }

eusouregislima commented 5 months ago

Algumas modificações foram feitas nesse ponto que começaram a ser um pouco confusas, mas vamos lá

Criamos o Schema de Orders na pasta schemas. Analisando a montagem do schema de Orders, vimos que ele recebe quem é o usuário dono do pedido. E para não precisar passar o usuário, pegamos os dados do token, apenas adicionando o decoded.name no auth para passar o nome também Dessa forma : request.userName = decoded.name; Lá no SessionController adicionamos name: user.name no token para passar o nome.

Na sequencia passamos o produto adicionando a quantidade e o status,. Também o timestamp para pegar a data de criação e update. Em seguida criamos o arquivo OrderController para iniciar as configurações

Para testar passamos apenas os dados abaixo para ver se estava pegando o nome: const { products } = request.body;

const order = {
  user: {
    id: request.userId,
    name: request.userName,
  },
};

Como houve sucesso, configuramos o arquivo para receber um array de produtos com objetos com id e quantidade do produto. Somente id, pq com o id conseguimos pegar os dados do produto direto no banco. Isso por questões de segurança, para que o meu usuário não consiga modificar por exemplo o preço de um produto no front-end. Validamos a chegada dos dados Pegamos o products do body, criamos uma rota /orders para testar o OrderController, e verificamos se os dados chegavam tudo ok

Também precisamos criar uma nova session para pegar o token atualizado. Aproveitamos para criar uma nova variável de ambiente com o token no canto superior direito e importamos essa variável em todas as rotas autenticadas.

eusouregislima commented 5 months ago

Parte 2 Vamos agora inserir o passo a passo do pensamento de desenvolvimento dessa parte

Lendo o arquivo OrderController passo a passo

eusouregislima commented 5 months ago

Nessa etapa para guardar os dados no banco nós importamos o schema de Order e adicionamos essa linha: const createdOrder = await Order.create(order); Respondendo com o createdOrder.

Instalamos o mongoDbCompass para ver os dados salvos

eusouregislima commented 5 months ago

Criando o update do status dos pedidos

Criar a rota routes.put('/orders/:id', OrderController.update); Essa rota receberá o id do pedido no params

Criar método update dentro do controller

async update(request, response) { const schema = Yup.object({ status: Yup.string().required(), });

try {
  schema.validateSync(request.body, { abortEarly: false });
} catch (err) {
  return response.status(400).json({ error: err.errors });
}

const { id } = request.params;
const { status } = request.body;

try {
  await Order.updateOne({ _id: id }, { status });
} catch (error) {
  return response.status(400).json({ error: error.message });
}

return response.json({ message: 'Status updated sucessfully' });

}

Esse método recebe o novo status que é uma string Verifica se o status chegou Pega o id na rota e o status no body Verifica se o id do pedido enviado é válido, e se sim faz o updateOne no id enviado alterando o status.

eusouregislima commented 5 months ago

Etapa de validação do usuário admin para ele poder acessar algumas rotas

-> Adicionado o seguinte trecho const { admin: isAdmin } = await User.findByPk(request.userId);

if (!isAdmin) {
  return response.status(401).json();
}

Que desestrutura o isAdmin pegando do model de User a propriedade admin. Se for falso não consegue acessar as rotas que esse trecho estiver presente.

eusouregislima commented 5 months ago

Trabalhando com o update de produtos

Adicionando o atributo offer nos produtos para listar as ofertas Adicionando essa propriedade como boolean no model -> offer: Sequelize.BOOLEAN,

Adicionado o método update no controller de produtos

async update(request, response) { const schema = Yup.object({ name: Yup.string(), price: Yup.number(), category_id: Yup.number(), offer: Yup.boolean(), });

try {
  schema.validateSync(request.body, { abortEarly: false });
} catch (err) {
  return response.status(400).json({ error: err.errors });
}

const { admin: isAdmin } = await User.findByPk(request.userId);

if (!isAdmin) {
  return response.status(401).json();
}

const { id } = request.params;

const findProduct = await Product.findByPk(id);

if (!findProduct) {
  return response
    .status(400)
    .json({ error: 'Make sure your product ID is correct.' });
}

let path;
if (request.file) {
  path = request.file.filename;
}

const { name, price, category_id, offer } = request.body;

await Product.update(
  {
    name,
    price,
    category_id,
    path,
    offer,
  },
  {
    where: {
      id,
    },
  },
);

return response.status(200).json();

}

Mudanças importantes: Ali no path para ficar opcional E o update ele modifica o que ele receber, se receber um, muda um, se receber todos, muda todos. Também foi criada uma rota para esse método

eusouregislima commented 5 months ago

Na última etapa em aulas, vamos colocar uma imagem na categoria Para isso adicionamos o path como string e a url no model de category, adicionando a rota caregory-file path: Sequelize.STRING, url: { type: Sequelize.VIRTUAL, get() { return http://localhost:3001/category-file/${this.path}; }, },

Modificamos também no controller para ele pegar o filename do request.file const { filename: path } = request.file;

E passar o path na criação da categoria.

Na rota de criação de categoria, adicionamos o upload.single('file')

E no app.js, dentro de middlewares adicionamos a rota que vai receber a imagem das categorias

this.app.use( '/category-file', express.static(resolve(__dirname, '..', 'uploads')), );

eusouregislima commented 5 months ago

Parte II -> Criando o update de categoria Criamos a rota de update de categorias e o método update no controller de categorias Bem similar ao update de product, e verificando se o nome da categoria já existe.

const { name } = request.body;

if (name) {
  const categoryNameExists = await Category.findOne({
    where: {
      name,
    },
  });

  if (categoryNameExists && categoryNameExists.id !== id) {
    return response.status(400).json({ error: 'Category already exists' });
  }
}

Nesse trecho ele verifica se o nome que enviei já existe, e se ele existe com o id que enviei, pq se envio o id 1, e o id 1 referesse a hamburguer, e o nome enviado foi hamburguer ele pode seguir em frente.

eusouregislima commented 5 months ago

No ato de conexão front com o back instalamos os cors e configuramos no projeto