Construindo aplicação do zero com node.js: Parte 5
Published: 2022-11-17 15:53:29

Faaaaaala devs, chegamos ao nosso último artigo, se você chegou até aqui, meus sinceros parabéns e muito obrigado.

## Mão na massa
Vamos começar configurando toda a nossa comunicação com o banco de dados, então dentro de **src/infra/persistence**, crie um arquivo chamado **database.js**, nele nós criaremos a nossa conexão com o banco:
```js
import { Sequelize } from 'sequelize';

export default class Database {
    static async getConnection() {
        const database = process.env.DB_DATABASE;
        const user = process.env.DB_USER;
        const password = process.env.DB_PASSWORD;
        const host = process.env.DB_HOST;
        const dialect = process.env.DB_DIALECT;

        const sequelize = new Sequelize(database, user, password, {
            host,
            dialect,
            logging: false
        });

        try {
            await sequelize.authenticate();
            console.log('Connection has been established successfully.');
            return sequelize;
        } catch (error) {
            console.error('Unable to connect to the database:', error);
        }
    }
}
```
Percebam que todas as informações foram definidas em nosso *.env*. Neste código, utilizei um método estático, mas se quiserem utilizar uma função ou qualquer outra forma, fique à vontade.

Em seguida, vamos definir o nosso modelo, que seria basicamente a representação de uma tabela do banco, crie um arquivo chamado **model.js**, com o conteúdo abaixo:
```js
import Database from './database.js';
import { DataTypes } from 'sequelize';

const loadModel = async () => {
    const connection = await Database.getConnection();
    const model = connection.define('User', {
        id: {
            type: DataTypes.INTEGER,
            required: true,
            primaryKey: true,
            autoIncrement: true
        },
        email: {
            type: DataTypes.STRING,
            required: true,
            allowNull: false
        },
        password: {
            type: DataTypes.STRING,
            required: true,
            allowNull: false
        },
        expired: {
            type: DataTypes.BOOLEAN,
            default: false
        },
        token: {
            type: DataTypes.STRING
        },
        email_token: {
            type: DataTypes.STRING
        }
    }, {
        tableName: 'users'
    });

    await model.sync();
    return model;
}

export default loadModel;
```
Um detalhe interessante, é que o código acima também vai funcionar como uma **migration**, isso é, ele vai criar a tabela no banco, com as colunas e tipos definidos acima. Vejam também que não adicionei muitas informações a essa tabela, se forem adequar este projeto em algum sistema, provavelmente vocês vão precisar adicionar mais alguma informação, e para isso, basta modificar este arquivo.

Agora que temos a nossa conexão e nosso modelo, vamos implementar as dependências que nosso domínio precisa, nesse caso aqui eu joguei tudo em um arquivo chamado **repository.js**:
```js
import IRepository from './../../domain/irepository.js';
import ITokenRepository from './../../domain/itoken-repository.js';
import User from './../../domain/user.js';

export class Repository extends IRepository {
    #model;

    constructor(model) {
        super();
        this.#model = model
    }

    async findByEmail(email) {
        const result = await this.#model.findOne({ where: {
                email
            }
        });

        if (!result) {
            return undefined;
        }

        return new User({
            id: result.dataValues.id,
            token: result.dataValues.token,
            emailToken: result.dataValues.email_token,
            email: result.dataValues.email,
            password: result.dataValues.password,
        })
    }

    async update(user) {
        const data = {
            token: user.token,
            email_token: user.emailToken,
            expired: user.expired
        }

        await this.#model.update(data, {
            where: {
                id: user.id
            }
        });
    }
}

export class TokenRepository extends ITokenRepository {
    #model;

    constructor(model) {
        super();
        this.#model = model
    }

    updateExpiredFieldToTrue(id) {
        throw Error('Must be implemented');
    }

    async findByToken(token) {
        const result = await this.#model.findOne({ where: {
                token: token.token,
                email_token: token.emailToken,
            }
        });

        if (!result) {
            return undefined;
        }

        return new User({
            id: result.dataValues.id,
            token: result.dataValues.token,
            emailToken: result.dataValues.email_token,
            email: result.dataValues.email,
            password: result.dataValues.password,
            expired: result.dataValues.expired
        })
    }

    async updateExpiredFieldToTrue(id) {
        const data = {
            expired: true
        }

        await this.#model.update(data, {
            where: {
                id
            }
        });
    }
}
```

Pode ser que por algum motivo, o nosso *database* não tenha sido criado corretamente, para que tenhamos a certeza, acesse no navegador o endereço: http://localhost:8081, que é a interface do *adminer*, selecione o sistema como *postgres*, e preencha os dados do servidor, usuário e senha, no meu caso ficou assim:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3uhagzctrtf9oqd10jqo.png)
Todas as informações preenchidas nessa configuração são as mesmas que estão no arquivo *.env*, definido no segundo artigo.

Clique em entrar, depois verifique se existe um *database* com o mesmo nome do campo **DB_DATABASE** definido em nosso *.env*, se não existir, clique em *Sql Command* e execute o comando *sql* abaixo:
```sql
create database two_factor;
```
**Só é necessário caso nosso _database_ não tenha sido criado.**

Agora que encerramos nossa parte de banco, vamos configurar nossas rotas. Dentro de **src/infra/http/hapi**, crie um arquivo chamado **server.js**, com o conteúdo abaixo:
```js
import Hapi from '@hapi/hapi';
import Vision from '@hapi/vision';
import Inert from '@hapi/inert';
import HapiSwagger from 'hapi-swagger';

const init = async () => {
    const server = Hapi.server({
        port: process.env.APP_PORT,
    });

    const swaggerOptions = {
        info: {
            title: 'API Two-Factor Authentication',
            version: 'v1.0',
        }
    };

    await server.register([
        Inert,
        Vision,
        {
            plugin: HapiSwagger,
            options: swaggerOptions
        }
    ]);

    await server.start();
    console.log('Server running on %s', server.info.uri);
    return server;
};

export default init;
```

Aqui adicionamos alguns plugins para o **hapi**, basicamente vai servir para gerarmos a documentação da nossa *api*, veremos isso com calma depois.

Agora vamos voltar a modificar o arquivo **index.js**, que está na raiz do nosso projeto, vamos importar nosso serviço de rotas:
```js
import loadEnv from './src/infra/env/load-env.js';
import init from './src/infra/http/hapi/server.js';

async function run() {
    await loadEnv();
    await init();
}

run();
```

Para saber se está tudo funcionando, basta ir no terminal do serviço node e executar o seguinte comando:
```bash
node index.js
```
O servidor vai subir, agora acesse o endereço: http://localhost:8001/, provavelmente vamos ter um retorno 404.

Agora vamos começar a chamar nossas regras e passar para elas todos os serviços criado anteriormente, para isso, crie o diretório **actions**, dentro de **src/infra**, e crie os arquivos **user-authetication-action.js** e **user-authetication-action.js**, com o conteúdo abaixo:
```js
// src/infra/action/token-authentication-action.js
import loadModel from '../persistence/model.js';
import { Repository } from '../persistence/repository.js';
import UserAuthentication from '../../domain/user-authentication.js';
import Email from '../email/email.js';
import PasswordHash from '../hash/password-hash.js';
import TokenService from '../token/token-service.js';
import LoginPayload from '../../domain/login-payload.js';

const createUserAuthentication = async (payload) => {
    const userModel = await loadModel();
    const repository = new Repository(userModel);
    const userAuthentication = new UserAuthentication(
        repository,
        new Email(),
        new PasswordHash(),
        new TokenService()
    );

    const loginPayload = new LoginPayload(payload.email, payload.password);
    return await userAuthentication.authenticate(loginPayload);
}

export default createUserAuthentication;

// src/infra/action/token-authentication-action.js
import loadModel from '../persistence/model.js';
import { TokenRepository } from '../persistence/repository.js';
import TokenAuthentication from '../../domain/token-authentication.js';
import Jwt from '../jwt/jwt.js';
import Token from '../../domain/token.js';

const createTokenAuthentication = async (payload) => {
    const userModel = await loadModel();
    const repository = new TokenRepository(userModel);
    const tokenAuthentication = new TokenAuthentication(
        repository,
        new Jwt()
    );

    const token = new Token(payload);
    return await tokenAuthentication.authenticate(token);
}

export default createTokenAuthentication;
```
Estamos chamando nossas lógicas de domínio e passando para elas todas as dependências implementadas, fizemos isso nos testes, só que lá injetamos comportamentos que simulam as ações.

Voltemos para diretório **src/infra/http/hapi**, nele vamos criar um arquivo chamado **routes.js**, onde vamos definir nossas rotas e suas ações:
```js
import Joi from 'joi';
import InvalidArgumentError from './../../../domain/invalid-argument-error.js';
import GatewayError from './../../../domain/gateway-error.js';
import Boom from '@hapi/boom';
import createUserAuthentication from '../../actions/user-authentication-action.js';
import createTokenAuthentication from '../../actions/token-authentication-action.js';

const handlerError = (error) => {
    if (error instanceof GatewayError) {
        return Boom.badGateway(error.message);
    }

    if (error instanceof InvalidArgumentError) {
        return Boom.badRequest(error.message);
    }

    return Boom.badImplementation(error);
}

const failAction = (request, h, err) => {
    throw err;
};

const routes = [
    {
        options: {
            tags: ['api'],
            description: 'Get temporary token',
            notes: 'Login with email and password',
            validate: {
                payload: Joi.object({
                    email: Joi.string().email().required(),
                    password: Joi.string().min(6).required()
                }),
                failAction
            }
        },
        method: 'POST',
        path: '/login',
        handler: async (request, h) => {
            try {
                const { payload } = request;
                const result = await createUserAuthentication(payload);
                return {
                    token: result
                };
            } catch (e) {
                return handlerError(e);
            }
        }
    },
    {
        options: {
            tags: ['api'],
            description: 'Login in the application',
            notes: 'Login with token and the token receive in e-mail',
            validate: {
                payload: Joi.object({
                    token: Joi.string().min(35).required(),
                    emailToken: Joi.string().min(22).required()
                }),
                failAction
            }
        },
        method: 'POST',
        path: '/token',
        handler: async (request, h) => {
            try {
                const { payload } = request;
                const result = await createTokenAuthentication(payload);
                return {
                    token: result
                };
            } catch (e) {
                return handlerError(e);
            }
        }
    }
]

export default routes;
```

Detalhando o código acima: a função **handlerError** vai trabalhar em cima do tipo de erro para retornar uma resposta personalizada, utilizamos o **Boom** para nos auxiliar nessas respostas. A constante **routes**, recebe um array de objetos contendo as duas rotas do nosso sistemas, ambas são do tipo **POST**, definimos uma tag para elas, isso é útil para o **swagger**, que vai documentar nossa api. Fizemos uso do **Joi** para validar os dados enviados pela requisição e finalmente chamamos nossas ações passando os dados da requisição, acredito que tenha ficado fácil de entender o código acima.

Agora a gente precisa informar essa rotas ao *hapi*, para isso, altere o arquivo **server.js**, e deixe ele assim:
```js
import Hapi from '@hapi/hapi';
import Vision from '@hapi/vision';
import Inert from '@hapi/inert';
import HapiSwagger from 'hapi-swagger';
import routes from './routes.js';

const init = async () => {
    const server = Hapi.server({
        port: process.env.APP_PORT,
    });

    const swaggerOptions = {
        info: {
            title: 'API Two-Factor Authentication',
            version: 'v1.0',
        }
    };

    await server.register([
        Inert,
        Vision,
        {
            plugin: HapiSwagger,
            options: swaggerOptions
        }
    ]);

    server.route(routes);

    await server.start();
    console.log('Server running on %s', server.info.uri);
    return server;
};

export default init;
```

E pronto, se tudo estiver ok, a gente pode acessar o seguinte endereço: http://localhost:8001/documentation, que vai carregar a documentação da api, com os campos a serem enviados, o tipo da requisição, etc, **lembre-se de parar e subir o servidor novamente**.

Se precisarmos modificar algo no código acima, a reinicialização do servidor é necessária, se quiserem evitar isso, basta configurar o uso do **nodemon**, conforme explicado no artigo anterior, ele vai identificar uma mudança e fazer o recarregamento do servidor. Se quiserem utilizá-lo, alterem o arquivo **package.json**, e adicione um novo *script*. O exemplo abaixo mostra a adição do campo *start* e o uso do *nodemon*:
```json
{
  "name": "two-factor-authentication",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
    "scripts": {
      "start": "node_modules/nodemon/bin/nodemon.js index.js",
      "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js ./tests/* --coverage --config='{ \"coverageReporters\": [\"html\"] }'\n"
    },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "dependencies": {
    "@hapi/boom": "^10.0.0",
    "@hapi/hapi": "^20.2.2",
    "@hapi/inert": "^7.0.0",
    "@hapi/vision": "^7.0.0",
    "bcrypt": "^5.1.0",
    "dotenv": "^16.0.3",
    "hapi-swagger": "^14.5.5",
    "joi": "^17.6.3",
    "jsonwebtoken": "^8.5.1",
    "nodemailer": "^6.8.0",
    "pg": "^8.8.0",
    "pg-hstore": "^2.3.4",
    "sequelize": "^6.25.3",
    "short-uuid": "^4.2.0"
  },
  "devDependencies": {
    "jest": "^29.2.1",
    "nodemon": "^2.0.20"
  }
}
```

Após essa alteração rode o seguinte comando:
```
npm start
```

Esse comando vai subir o servidor e se qualquer modificação for realizada, o servidor vai ser reinicializado automaticamente.

E antes de irmos para um postman ou qualquer outra ferramenta, vamos criar um teste para saber se tudo está funcionando, crie um arquivo de teste chamado **login.test.js**, dentro de **tests/feature**, com o conteúdo abaixo:
```js
import loadEnv from '../../src/infra/env/load-env.js';
import init from '../../src/infra/http/hapi/server.js';
let api = {};
let token = {};
let user = {
    email: 'email@email.com',
    password: '123456'
}
import Bcrypt from 'bcrypt';
import loadModel from '../../src/infra/persistence/model.js';
let model = {};
beforeAll(async () => {
    await loadEnv();
    api = await init();
    model = await loadModel();
    const pass = await Bcrypt.hash(user.password, 3);
    await model.destroy({ where : { email: user.email }});
    await model.create({
        email: user.email,
        password: pass
    });
});

test('Should get token', async () => {
    const result = await api.inject({
        method: 'POST',
        url: '/login',
        payload: user
    });
    const data = JSON.parse(result.payload);
    token = data.token;
    expect(token.length).toBeGreaterThan(35);
});

test('Should get jwt token', async () => {
    const user = await model.findOne({where: {token}, raw: true});
    const result = await api.inject({
        method: 'POST',
        url: '/token',
        payload: {
            token,
            emailToken: user.email_token
        }
    });
    const data = JSON.parse(result.payload);
    expect(data.token.length).toBeGreaterThan(40);
    expect(data.token).toBeTruthy();
    await model.destroy({ where : { email: user.email }});
});
```
Aqui, primeiro criamos um usuário temporário e depois começamos a fazer as requisições, primeiro com login e senha, e depois a requisição passando os tokens. Observe o uso da lib *Bcrypt* para fazer um hash da nossa senha.

Antes de executarmos o teste, duplique o conteúdo do arquivo *.env*, para um arquivo chamado **.env.test**, isso é bem útil pois podemos realizar testes utilizando outro banco por exemplo.

Agora sim podemos executar nossos testes mais uma vez:
```bash
npm t
```

Se você tiver seguido todos os passos, todos os testes vão passar corretamente. Minha dica é sempre olharem o relatório de cobertura.

# Fim
Bem devs, chegamos ao fim do nosso projeto, espero que tenham aprendido algo, assim como aprendi. No primeiro artigo da série, eu deixei o link do repositório do código do projeto.

Novamente deixo meus agredecimentos por terem chegado até aqui. Nos vemos em outros artigos, até mais.