GoBarber - Aplicação Backend

Minhas notas de aula e código do bootcamp da Rocketseat, nesse projeto criamos uma API com NodeJS

GoBarber

Nesse projeto vamos construir um API no backend utilizando NodeJS, Express, Postgres, Nodemon, Sucrase, entre outras libs. Essas são a minhas notas de aula do bootcamp da Rocketseat.

Tópicos:

  • Aula 1 - Configurando o Projeto
  • Aula 2 - Nodemon & Sucrase
  • Aula 3 - Conceitos de Docker
  • Aula 4 - Mão na Massa
  • Aula 5 - Sequelize & MVC
  • Aula 6 - Padrão de código - Eslint, Prettier & EditorConfig
  • Aula 7 - Configurando o Sequelize
  • Aula 8 - Migração de usuário
  • Aula 9 - Criação do Model de Usuário
  • Aula 10 - Loader de Models
  • Aula 11 - Criando usuário
  • Aula 12 - Enviando password_hash
  • Aula 13 - Conceitos de JWT
  • Aula 14 - Autenticação JWT
  • Aula 15 - Middleware de autenticação
  • Aula 16 - Update do usuário
  • Aula 17 - Validando dados de entrada

Código do projeto no Github.

Aula 1 - Configurando o Projeto

Uma maneira legal de estruturar o backend é utilizar Classes.

Nesse commit foi criado um projeto com node, usando yarn init -y e instalado a dependência doexpress.

Foi estruturado as pastas, separando a lógica da aplicação. As rotas são middlewares também, porém, elas ficam separadas dos middlewares para dar mais semântica.

O servidor foi inicializado de dentro do server.js onde a instância do app foi importada, isso desacopla para facilitar no teste.

Veja o código: https://github.com/tgmarinho/gobarber/tree/aula1

Aula 2 - Nodemon & Sucrase

Para utilizar o import/export podemos utilizar o babel ou outras ferramentas, mas nesse projeto iremos utilizar sucrase que é bem rápido e fácil.

yarn add sucrase nodemon -D

Pronto agora só alterar para import/export

Entretanto, não podemos mais rodar node src/index.js para executar o projeto.

Pode ser assim: yarn sucrase-node src/server.js

Mas eu quero utilizar o nodemon também.

Nodemon detecta atualização no código e reinicializa o servidor.

Preciso criar um arquivo `nodemon.json na raiz do projeto, com a seguinte configuração:

{
  "execMap": {
    "js": "sucrase-node"
  }
}

Lá no package.json crio um script:

"scripts": {
    "dev": "nodemon src/server"
  },

e agora posso executar o projeto com: yarn dev

Veja o código: https://github.com/tgmarinho/gobarber/tree/aula2

Aula 3 - Conceitos de Docker

Docker ajuda a controlar os serviços externos da aplicação: Banco de Dados, Redis, etc.

Como funciona?

  • Criação de ambientes isolados (container)

    • Você baixa uma imagem com ambiente configurado, você não precisa instalar os softwares na sua máquina e alterar o seu sistema operacional. Quando a gente instala o postgres com Docker ele se torna um subsistema, e fica rodando na máquina virtual do Docker, sem interferir o ambiente, isso é ótimo porque podemos replicar o mesmo ambiente de desenvolvimento ou produção em outras máquinas, sem problema de arquitetura ou diferença em SO.
    • Os containers expõe as portas para podemos nos conectar nos containers.
    • Instalando o postgres, sempre usamos a porta :5432, com mongoDB seria na porta :27017, mas trocar portas no Docker é muito simples.

Principais conceitos

  • Imagem: São os principais serviços que iremos utilizar, ex: postres, mongodb, redis, etc.
  • Container: é uma instância de uma imagem, se tivermos uma imagem do mongodb, podemos criar um ou mais containers do mongodb, até mesmo para servir para outras aplicações na máquina
  • Docker Registry (Docker Hub) é onde podemos visualizar e baixar as imagens (ISOs). é basicamente o repositório de imagens, inclusive podemos criar as nossas próprias imagens e hospedar lá.
  • Dockerfile

    • Receita de uma imagem: Define como a imagem da nossa aplicação irá funcionar em um outro computador, em um ambiente do zero.
    • Dockerfile de exemplo para executar a nossa aplicação:
# Partimos de uma imagem existente
FROM node:10
# Definimos a pasta e copiamos o arquivos
WORKDIR /usr/app
COPY . ./
# Instalamos as dependências
RUN yarn
# Qual porta queremos expor?
EXPOSE 3333
# Executamos nossa aplicação
CMD yarn start

Fim: https://github.com/tgmarinho/gobarber/tree/aula3

Aula 4 - Mão na Massa

Baixar o docker (Mac, Linux, Windows): https://docs.docker.com/install/

Para ver se está instalado docker -v ou docker -help

Repositório de imagens do Docker: https://hub.docker.com/

Instalando o postgres: https://hub.docker.com/_/postgres

❯ docker run --name database -e POSTGRES_PASSWORD=docker -p 5432:5432 -d postgres

Redirecionamento de Portas, toda vez que algum serviço for chamado na porta 5432 do servidor, será redirecionado para 5432 do container no docker: -p 5432:5432

Se já estiver com postgres na máquina sem ter sido instalado pelo Docker, e se estiver executando, você pode alterar na aplicação para: -p 5433:5432, isto é, quando for chamado o serviço do postgres 5433, vai ser redirecionado para a porta padrão de dentro do Docker: 5432. Muito legal esse desacoplamento.

Quando já se tem uma imagem no Docker:

pasta executar uma imagem:

❯ docker run -d 30bf4f039abe

Para executar um container:

docker  start a46a366365bb

Esses caracteres estranhos é o ID da imagem, para ver basta digitar:

❯ docker image ls
❯ docker image ls
REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
kartoza/postgis           latest              5a242bc9bf9f        4 months ago        990MB
redis                     alpine              c8eda26fcdab        5 months ago        50.9MB
mongo                     latest              a3abd47e8d61        6 months ago        394MB
mongoclient/mongoclient   latest              436b2a2bbe16        6 months ago        1.11GB
adminer                   latest              709d7ce11f75        6 months ago        83.2MB
postgres                  latest              30bf4f039abe        6 months ago        312MB
mongo                     4                   0da05d84b1fe        7 months ago        394MB

Vai listar todas as imagens e seus respectivos IDs.

E para conferir se está rodando, só rodar docker ps, com isso ele vai listar todos os containers que estão em execução:

❯ docker ps                 
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
6f0b42548e9e        30bf4f039abe        "docker-entrypoint.s…"   2 minutes ago       Up 2 minutes        5432/tcp            goofy_hopper
~ 

Agora ver o banco funcionando, pode conectar com linha de comando no terminal ou instalar uma GUI:

Linux, Mac e Windows: https://electronjs.org/apps/postbird ou Mac: https://eggerapps.at/postico/

Só usar os dados da conexão para poder conectar-se no postgres.

e criar o banco de dados: `create database gobarber

Quando reinicia a máquina, o docker para, para subir novamente só seguir os comandos:

docker ps -a para mostrar todos os container mesmo os que não estão em execução.

e depois executar o comando para subir o container:

docker  start postgres 

Pode ser o ID ou o nome do container.

Para ver os logs do container:

docker logs postgres

O mesmo container pode ser usado para outras aplicações, mas tem como fazer um container apenas para a aplicação.

Para remover um container:

docker rm "ID ou nome do container"

Fim: https://github.com/tgmarinho/gobarber/tree/aula4

Aula 5 - Sequelize & MVC

Sequelize é um ORM (Object-relational mapping), basicamente ele faz o mapeamento dos objetos como entidade no banco de dados. Os bancos de dados tem um conceito de Entidade, Tabelas, Atributos, e a aplicação tem o conceito de Objetos, Atributos ou propriedades e métodos ou função. O que o ORM faz é mapear o objeto, criando uma tabela e os atributos mapeando para campos do banco de dados. O Sequelize também ajuda a fazer as consultas do banco de dados, em vez de usar SQL nativo, podemos usar objetos com seus respectivos métodos, e escrever javascript para fazer operações de CRUD persistência no BD.

As tabelas do banco de dados se transformam em Models (MVC)

no banco de dados temos:

users, products, productsItem

e no no JS teremos Users.js, Products.js, ProductsItem.js.

Diferença entre SQL e SequelizeSQL:

SQL:

INSERT INTO users (name, email) VALUES ("Thiago Marinho", "tgmarinho@gmail.com")
SELECT * FROM users WHERE email = "tgmarinho@gmail.com" LIMIT 1

Sequelize:

User.create({ name: 'Diego Fernandes' , email: 'diego@rocketseat.com.br' , })
User.findOne({ where: { email: 'tgmarinho@gmail.com' } })

No Sequelize temos também as Migrations:

  • Controle de versão para base de dados:

    • Cada alteração na tabela como adição, remoção de campos ou criação de novas tabelas, é nas migrations que criamos a estrutura. É um controlador de versão mesmo, pode fazer rollback para desfazer alguma coisa, no banco de dados fica um registro de versão de cada migration que é executada.
  • Cada arquivo contém instruções para criação, alteração ou remoção de tabelas ou colunas;
  • Mantém a base atualizada entre todos desenvolvedores do time e também no ambiente de produção;
  • Cada arquivo é uma migration e sua ordenação ocorre por data;

Exemplo de Migration:

module.exports = {
    up: (queryInterface, Sequelize) != {
        return queryInterface.createTable('users', {
            id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER
            },
            name: {
                allowNull: false,
                type: Sequelize.STRING
            },
            email: {
                allowNull: false,
                unique: true,
                type: Sequelize.STRING
            }
        })
    },
    down: (queryInterface, Sequelize) != {
        return queryInterface.dropTable('users')
    }
}

https://sequelize.org/master/manual/migrations.html

Obs:

  • É possível desfazer uma migração se errarmos algo enquanto estivermos desenvolvendo a feature;
  • Depois que a migration foi enviada para outros devs ou para ambiente de produção ela JAMAIS poderá ser alterada, uma nova deve ser criada;
  • Cada migration deve realizar alterações em apenas uma tabela, você pode criar várias migrations para alterações maiores;

Temos também os Seeds

• População da base de dados para desenvolvimento: - Podemos utilizar ele para gerar dados em tempo de execução do projeto, quando subimos o projeto ele cria dados fake. • Muito utilizado para popular dados para testes; • Executável apenas por código; • Jamais será utilizado em produção; - A ideia aqui é usar apenas os dados fake, para testar o fluxo do sistema e também performance de listas, etc, tem várias libs em JS que geram dados fake que pode ser usado nos Seeds. • Caso sejam dados que precisam ir para produção, a própria migration pode manipular dados das tabelas; - Os dados que vão para produção devem estar nas Migrations e não no Seed.

Arquitetura MVC

Model, View, Controller é um arquitetura bem antiga e utilizado nos dias de hoje, onde:

M = Model = Código da estrutura do banco de dados utilizando ORM ou não; V = View = Código HTML, CSS, JS, JSX, código de criação e manipulação das telas do site/app; C = Controller = Código JS, que contém a lógica do negócio, é o intermediário entre o Model e a View

M <-> C <-> V

A View faz a requisição, o Controller recebe, processa, chama o banco de dados(Model) o banco retorna para o Controller e repassa para a View a resposta, a qual é renderizada para o usuário.

Exemplo de um Controller:

class UserController {
 index() { } !/ Listagem de usuários
 show() { } !/ Exibir um único usuário
 store() { } !/ Cadastrar usuário
 update() { } !/ Alterar usuário
 delete() { } !/ Remover usuário
}

Uma boa prática de criação de Controller, na estrutura REST e MVC em geral, é que o controller só pode ter os cinco métodos abaixo, ou menos, mais que isso não. Se sentir a necessidade de ter outro método, é porque na verdade você tem que criar um outro objeto, exemplo: SessionController.js, LoginController.js.

Aula 6 - Padrão de código - Eslint, Prettier & EditorConfig

Padrão de código é muito útil quando se está trabalhando com um time, pq cada um pode fazer as coisas de sua maneira e a base de código não vai ficar muito boa, tem desenvolvedor que vai usar var, outro const, e outro let, vai pular linha no final, outro não vai, vai usars export default, e outro não, e isso gera uma bagunça, e aí com isso temos algumas ferramentas que ajudam a definir as regras(padrão) de código e um estilizador de código utilizando as regras, e o mais adotado na comunidade JS, é o Eslint para definir as regras e o Prettier para formatar o código conforme as regras definidas no Eslint.

Mas o ESLINT tbm não tem qualquer regra, você pode usar algumas mais utilizadas no mercado, como as guias de estilos do Airbnb, Standard e outros, cada um tem suas características e estilos de escrita, ai vai do gosto de cada um, e do padrão que seu framework adota também, no caso Adonis utilizar o Stardard, então é sugestivo você usar o Standard para criar seu projeto seguindo esse estilo.

Configurando o projeto

Adicionando o eslint como dependências de desenvolvimento:

 yarn add eslint -D

Feito isso só inicializar o eslint:

yarn eslint --init

Ele vai fazer algumas perguntas e você pode configurar a seu gosto, eu usei a terceira opção:

gobarber on  aula6 [!] took 10s 
❯ yarn eslint --init
yarn run v1.12.0
$ /Users/tgmarinho/Developer/bootcamp_rocketseat_studies/gobarber/node_modules/.bin/eslint --init
? How would you like to use ESLint? 
  To check syntax only 
  To check syntax and find problems 
❯ To check syntax, find problems, and enforce code style 

E só seguir respondendo o Eslint. No final ele pede para instalar as dependências, só instalar e remover o package-lock.json e executar um yarn para atualizar as dependências, isso eu faço pq não estou usando o npm e sim o yarn como gerenciador de dependência e o eslint em baixo dos panos usa o npm para instalar.

No final ele cria um arquivo: .eslintrc.js com as seguintes configurações padrão:

module.exports = {
    env: {
        es6: true,
        node: true,
    },
    extends: [
        'airbnb-base',
    ],
    globals: {
        Atomics: 'readonly',
        SharedArrayBuffer: 'readonly',
    },
    parserOptions: {
        ecmaVersion: 2018,
        sourceType: 'module',
    },
    rules: {},
};

OBS: Preciso ter o eslint nas extensões do VSCode.

E para o eslint corrigir automaticamente quando salva o arquivo, precisa ter nas settings.json do VSCode a seguinte configuração:

  "eslint.autoFixOnSave": true,
  
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    {
      "language": "typescript",
      "autoFix": true
    },
    {
      "language": "typescriptreact",
      "autoFix": true
    }
  ],

Pronto, agora já deve estar ok. Se o VSCode não estiver acusando erro de Eslint no arquivo app.js, pois com padrão Airbnb as aspas tem que ser simples, e deve ter ; no final de cada comando. Então fecha o vscode e abre novamente, ou tenta remover a nodemodules e instalar novamente: `rm -rf nodemodules/ yarn.lock && yarn`.

No .eslintrc.js, teremos uma definição de novas regras, é tipo como subscrever as regras padrão da guia de estilo airbnb no eslint, isso é necessário algumas vezes devido algum framework que iremos utilizar no dia a dia. .eslintrc.js:

 rules: {
    "class-methods-use-this": "off",
    "no-param-reassign": "off",
    camelcase: "off",
    "no-unused-vars": ["error", { argsIgnorePattern: "next" }]
  }

Instalando o Prettier

O Prettier melhora o código, deixando mais bonito, ele faz uma estilização a mais no código, além do que o eslint já faz.

 yarn add prettier eslint-config-prettier eslint-plugin-prettier -D

e no .eslintrc.js preciso declarar:

extends: ["airbnb-base", "prettier"],
plugins: ["prettier"],
 rules: {
    "prettier/prettier": "error",
    "class-methods-use-this": "off",
    "no-param-reassign": "off",
    camelcase: "off",
    "no-unused-vars": ['error', { argsIgnorePattern:  'next' }],
  }

Com isso o prettier está pronto para ser usado, porém, tem algumas regras de conflito entre o prettier e airbnb, então precisa de mais configuração, para desabilitar as configurações que o prettier sobrescreve do airbnb.

Criar o arquivo: prettier.rc:

{
	"singleQuote":  true,
	"trailingComma":  "es5"
}

Defini a regra para manter aspas simples e deixar ; no final de cada instrução de código. Done!

Para corrigir todos os arquivos é só rodar:

yarn eslint --fix src --ext .js

Legenda: --fix conserta tudo que está na pasta src que tenha a extensão(--ext) de arquivos .js.

Podemos colocar ela no package.json:

 "scripts": {
    "dev": "nodemon src/server",
    "eslintify": "yarn eslint --fix src --ext .js"
  },

e rodar : yarn eslintify

Com isso, agora temos como manter o padrão de código na base de código da aplicação, se receber algum warn ou error, só ajustar conforme a sugestão do ESlint.

Mas e se os outros desenvolvedores não usam a IDE VSCode? Usam o Sublime, Vim, Atom ou WebStorm?

Ai entra o EditorConfig

Ele serve para que as regras definidas no Eslint e Prettier sejam aplicadas para todos as IDEs.

para isso basta criar um arquivo: .editorConfig na raiz do projeto com as configurações:

root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

Fim: https://github.com/tgmarinho/gobarber/tree/aula6

Aula 7 - Configurando o Sequelize

  • Criar estrutura de pastas, dentro da src
  • adicionar a dependência: yarn add sequelize no projeto
  • adicionar a interface de linha de comando do sequelize: yarn add sequelize-cli -D
  • criar o arquivo .sequelizerc na raiz do projeto para poder configurar os caminhos para as pastas de models, config, para rodar os comandos sequelize-cli:
const { resolve } = require('path');

module.exports = {
  config: resolve(__dirname, 'src', 'config', 'database.js'),
  'models-path': resolve(__dirname, 'src', 'app', 'models'),
  'migrations-path': resolve(__dirname, 'src', 'database', 'migrations'),
  'seeders-path': resolve(__dirname, 'src', 'database', 'seeds'),
};

Configurando o database:

  • adiciono as dependencias: yarn add pg pg-hstore
  • no arquivo config/database.js:
module.exports = {
  dialect: 'postgres',
  host: 'localhost',
  username: 'postgres',
  password: 'docker',
  database: 'gobarber',
  define: {
    timestamps: true, // garante que será criado um atributo: created_at e updated_at na tabela do banco de dados.
    underscored: true, // permite o ORM criar nome de tabelas como products_item
    underscoredAll: true, // permite o ORM criar nome dos atributos com caixa baixa e _ em vez de camelCase, pois esse é a convenção de escrita no banco de dados
  },
};

Fim: https://github.com/tgmarinho/gobarber/tree/aula7

Aula 8 - Migração de usuário

Para criar as migrations basta rodar comando:

yarn sequelize migration:create --name=create-users 

Com isso ele vai criar um arquivo:

20190913144153-create-users.js

Com um template:

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    /*
      Add altering commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.createTable('users', { id: Sequelize.INTEGER });
    */
  },

  down: (queryInterface, Sequelize) => {
    /*
      Add reverting commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.dropTable('users');
    */
  },
};

Método up quando a migration é executada e método down para fazer um rollback.

Migration de Usuário

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('users', {
      id: {
        type: Sequelize.INTEGER,
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
      },
      name: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      email: {
        type: Sequelize.STRING,
        allowNull: false,
        unique: true,
      },
      password_hash: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      provider: {
        type: Sequelize.BOOLEAN,
        defaultValue: false,
        allowNull: false,
      },
      created_at: {
        type: Sequelize.DATE,
        allowNull: false,
      },
      updated_at: {
        type: Sequelize.DATE,
        allowNull: false,
      },
    });
  },

  down: queryInterface => {
    return queryInterface.dropTable('users');
  },
};

Para rodar a migration:

❯ yarn sequelize db:migrate
yarn run v1.12.0
$ /Users/tgmarinho/Developer/bootcamp_rocketseat_studies/gobarber/node_modules/.bin/sequelize db:migrate

Sequelize CLI [Node: 10.16.3, CLI: 5.5.1, ORM: 5.18.4]

Loaded configuration file "src/config/database.js".
== 20190913144153-create-users: migrating =======
== 20190913144153-create-users: migrated (0.040s)

✨  Done in 1.02s.

E ai podemos ver o DDL lá na GUI do Postgres:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name character varying(255) NOT NULL,
    email character varying(255) NOT NULL UNIQUE,
    password_hash character varying(255) NOT NULL,
    provider boolean NOT NULL DEFAULT false,
    created_at timestamp with time zone NOT NULL,
    updated_at timestamp with time zone NOT NULL
);

-- Indices -------------------------------------------------------

CREATE UNIQUE INDEX users_pkey ON users(id int4_ops);
CREATE UNIQUE INDEX users_email_key ON users(email text_ops);

Além da tabela users, é criada uma tabela SequelizeMeta que tem os registros de cada migration que foram executadas.

Para desfazer as migrations:

yarn sequelize db:migrate:undo                      

Com isso a tabela users não existirá mais.

Desfazer tudo, com isso desfazer todas as migrations que foram executadas e não apenas a última.

yarn sequelize db:migrate:undoAll                      

Fim: https://github.com/tgmarinho/gobarber/tree/aula8

Aula 9 - Criação do Model de Usuário

Criar um model o sequelize, dentro da pasta models, criar uma arquivo: user.js:

import Sequelize, { Model } from 'sequelize';

class User extends Model {
  static init(sequelize) {
    super.init(
      {
        name: Sequelize.STRING,
        email: Sequelize.STRING,
        password_hash: Sequelize.STRING,
        provider: Sequelize.BOOLEAN,
      },
      {
        sequelize,
      }
    );
  }
}

export default User;

Criei a classe User que extend de Model do sequelize para receber todos métodos que tem na Model, agora a nossa classe de domínio User é uma Model, devido a herança. De dentro do método estático init passo uma parametro sequelize do Model, que será a conexão do banco de dados, e chamo o super da model, informando os atributos da tabela users, e com isso é feito o mapeamento do ORM entre as entidades do banco de dados e o objeto da aplicação, para entidade User.

Fim: https://github.com/tgmarinho/gobarber/tree/aula9

Aula 10 - Loader de Models

Para conectar a aplicação com banco de dados e carregar os models, temos que criar um o arquivo index.js na pasta database.

import Sequelize from 'sequelize';
import User from '../app/models/User';
import databaseConfig from '../config/database';

const models = [User];

class Database {
  constructor() {
    this.init();
  }

  init() {
    this.connection = new Sequelize(databaseConfig);
    models.map(model => model.init(this.connection));
  }
}
export default new Database();

Quando esse arquivo é importado, ele recebe uma instância do Database, que chama a função init, que instancia para o this.connection a Sequelize com as configurações de conexão com banco de dados. E para cada model que eu importei eu passo a conexão.

E agora só testar, veja o código da aula.

// Quando chamo a rota '/', cadastro o usuário e retorno os dados do banco de dados
// http://localhost:3333/

{
  "id": 1,
  "name": "Thiago Marinho",
  "email": "tgmarinho@gmail.com",
  "password_hash": "1232131",
  "updatedAt": "2019-09-13T15:39:29.116Z",
  "createdAt": "2019-09-13T15:39:29.116Z",
  "provider": false
}

Fim: https://github.com/tgmarinho/gobarber/tree/aula10

Aula 11 - Criando usuário

  • Criar controller de Usuário
  • Criar a rota de users para receber a requisição e passar o UserController.store para que seja executado quando a rota for chamada.
  • Validar se já existe o usuário, pois o email é um campo único na base de dados (validação no backend é importante)

Fim: https://github.com/tgmarinho/gobarber/tree/aula11

Aula 12 - Enviando password_hash

Quando o usuário digita a senha e envia para o controllers, queremos que seja gerado um hash para salvar a senha no banco de dados, e posteriormente quando ele for fazer login, ele digita a senha normal, e geramos um hash e comparamos com o hash que foi salvo no password_hash do banco de dados, se for igual, ok, está autenticado.

Para fazer isso precisamos de uma lib para gerar o hash do password:

yarn add bcryptjs 

Bcryptjs é utilizado no model de User, criamos um campo virtual, que é utilizado para receber o password do frontend e que é feito o hasg para através da lib bcrypt para a variável password_hash que essa sim é uma String que é salva no banco de dados.

Fim: https://github.com/tgmarinho/gobarber/tree/aula12

Aula 13 - Conceitos de JWT

Json Web Token(JWT) , server para fazer autenticação.

JWT (JSON Web Token) é um sistema de transferência de dados que pode ser enviado via POST ou em um cabeçalho HTTP (header) de maneira “segura”, essa informação é assinada digitalmente por um algoritmo HMAC, ou um par de chaves pública/privada usando RSA. (Saiba mais...)

Gera um token com headers (tipo de token, algoritmo), Payload (Dados adicionais) e Assinatura ( o que garante a veracidade do token, não pode ser modificado).

Fim: https://github.com/tgmarinho/gobarber/tree/aula13

Aula 14 - Autenticação JWT

Vamos usar a lib jsonwebtoken:

yarn add jsonwebtoken

Para criar a autenticação do usuário, podemos criar um controller: SessionController.js que serve para tratar a autenticação e não a criação de usuário.

Para gerar string aleatória (secret). https://www.md5online.org/

Fim: https://github.com/tgmarinho/gobarber/tree/aula14

Aula 15 - Middleware de autenticação

Criar um Middleware para bloquear o usuário se ele não estiver logado na aplicação.

Para garantir que o usuário está logado, ele tem que ter o token no header.

Então quando for chamar a rota de update usando o método put na rota /users, antes de chamar o UserController.update, tem que chamar o authMiddleware, que vai verificar se o token está presente na requisição.

Código do Middleware: app/middleware/auth.js:

export default (req, res, next) => {
  const authHeader = req.headers.authorization;
  console.log(authHeader);

  next();
};

routes.js:

routes.put('/users', authMiddleware, UserController.update);

Ou podemos utilizar de forma global, e toda a rota abaixo de use(authMiddleware) devem ser autenticadas:

import { Router } from 'express';
import UserController from './app/controllers/UserController';
import SessionController from './app/controllers/SessionController';
import authMiddleware from './app/middlewares/auth';

const routes = new Router();

routes.post('/users', UserController.store);
routes.post('/sessions', SessionController.store);

// Todas as rotas que forem chamadas a partir daqui tem que ser autenticada
routes.use(authMiddleware);
routes.put('/users', UserController.update);

export default routes;

Middleware de autenticação completo:

import jwt from 'jsonwebtoken';
import { promisify } from 'util';
import authConfig from '../../config/auth';

export default async (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    return res.status(401).json({ error: 'Token not provided' });
  }

  const [, token] = authHeader.split(' ');

  try {
    const decoded = await promisify(jwt.verify)(token, authConfig.secret);

    req.userId = decoded.id;
    return next();
  } catch (error) {
    return res.status(401).json({ error: 'Token invalid' });
  }
};

Estou utilizando promisfy do node para transformar o callback do jwt.verify em promise e poder utilizar o async/await para poder resolver a promise, fica muito melhor. Invocando o promisy, passo a função que contém o callback, ele me retorna uma verify em promise e passos os valores da função. E assim verifico se o token é valido.

O decode recebe o ID do usuário por foi o atributo que foi passado como payload na geração do token (ver arquivo SessionController.js):

token: jwt.sign({ id }, authConf.secret, {
	expiresIn: authConf.expireIn,
}),

Além do id, o decode recebe o exp: que é a data e hora em timestamp que o token irá expirar.

Foi feito uma trick do JS para remover o Baerer da string do token, o token vem como uma String:

Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NiwiaWF0IjoxNTY4NDA1MDAyLCJleHAiOjE1NjkwMDk4MDJ9.NPwa4vr80wAeEJvX9XWNMQAsUWXaDoSUwuw1KAR4wVw

E precisamos apenas do valor do token, para isso desestruturamos o array, que foi gerado pelo split(' '), que cortou a string em dois, pelos espaços (que no caso só tem um espaço na string) e retornou o array:

const [, token] = authHeader.split(' ');
['Bearer','eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NiwiaWF0IjoxNTY4NDA1MDAyLCJleHAiOjE1NjkwMDk4MDJ9.NPwa4vr80wAeEJvX9XWNMQAsUWXaDoSUwuw1KAR4wVw']

E como não precisamos do Baerer, então fazemos apenas: const [, token], menosprezamos o valor da primeira posição, pois a palavra Baerer é insignificante no nosso contexto e ficamos apenas com o token.

E por fim pegamos o ID do usuário que estava no payload do token e guardamos na requisição:

req.userId = decoded.id;

E agora com isso no método update do UserController.js teremos acesso, e em todos os controllers que passarem pela verificação de autenticação:

UserController.js:

 async update(req, res) {
    console.log(req.userId);
    return res.json({ ok: true });
}

Fim: https://github.com/tgmarinho/gobarber/tree/aula15

Aula 16 - Update do usuário

Código do update do usuário no arquivo: UserController.js

  async update(req, res) {
    const { email, oldPassword } = req.body;
    const user = await User.findByPk(req.userId);
    if (email !== user.email) {
      const userExists = await User.findOne({
        where: { email },
      });
      if (userExists) {
        return res.status(400).json({ error: 'User already exists.' });
      }
    }
    // só faço isso se ele informou a senha antiga, isto é, quer alterar a senha
    if (oldPassword && !(await user.checkPassword(oldPassword))) {
      return res.status(401).json({ error: 'Password does not match.' });
    }

    const { id, name, provider } = await user.update(req.body);

    return res.json({ id, name, email, provider });
  }

Nesse código:

  • Sempre tenho que ter o email do usuário;
  • Usuário pode informar a senha antiga, caso queira alterar a senha;
  • Pode alterar todos os atributos;
  • É feito uma verificação se a senha antiga é a senha atual do usuário
  • Aproveito a instância de user que veio do banco de dados no findByPk e altero os dados do usuário, o qual retorna os novos usuário
  • Envio via json todos os dados do usuário, exceto a senha.

Fim: https://github.com/tgmarinho/gobarber/tree/aula16

Aula 17 - Validando dados de entrada

Vamos validar os dados do usuário, é uma boa prática ter a validação do usuário no frontend no backend, a vantagem de estar no frontend é que a validação é mais rápida, não precisa ir diretamente no servidor para poder verificar se tem algum dado errado ou faltando, ganha em velocidade, também em menos tráfego ao servidor e principalmente na segurança. Ter só a validação no frontend não é uma boa prática, na verdade é uma péssima prática.

Vamos validar o frontend com biblioteca Yup, que faz uma validação no schema, Schema Validation:

yarn add yup

Trecho de código com Yup Validation

Esse código é do método update no UserController.js

import  *  as Yup from  'yup';

  const schema = Yup.object().shape({
      name: Yup.string(),
      email: Yup.string().email(),
      oldPassword: Yup.string().min(6),
      password: Yup.string()
        .min(6)
        .when('oldPassword', (oldPassword, field) =>
          oldPassword ? field.required() : field
        ),
      confirmPassword: Yup.string().when('password', (password, field) =>
        password ? field.required().oneOf([Yup.ref('password')]) : field
      ),
    });

Uso o Yup para criar um schema, passado um objeto com o corpo definido por mim, detalhe para campos condicionais, caso o oldPassword é informado o campo password deve ser obrigatório (required), o field no segundo parâmetro é o password.

Mas se o usuário digitar a senha, ele precisa confirmar a senha, então ele informa o confirmPassword, e ambos precisam ser iguais, então uso a função oneOf que recebe um array, e o Yup tem a referências de todos os campos, então uso: Yup.ref('password').

Defino o schema, agora é só validar com os dados que vieram da requisição (req.body):

if (!(await schema.isValid(req.body))) {
	return res.status(400).json({ error:  'Validation fails' });
}

o método é assíncrono então uso await, se tiver algo que não atende os requisitos do Schema Validation então retorna uma mensagem para o usuário com error: 'Validation Fails'.

Podemos validar os dados informados no Login, dentro do SessionController.js:

  const schema = Yup.object().shape({
      email: Yup.string()
        .email()
        .required(),
      password: Yup.string().required(),
    });

    if (!(await schema.isValid(req.body))) {
      return res.status(400).json({ error: 'Validation fails' });
    }

Aqui verificamos apenas se o usuário informou o email e senha.

Fim: https://github.com/tgmarinho/gobarber/tree/aula17

Fim da primeira parte da construção da API! Muito massa \o/

Comentários