├── .gitignore ├── .eslintrc.json ├── src ├── auth │ ├── confs.js │ ├── token.auth.js │ ├── strategies │ │ └── JWT.js │ └── authenticate.auth.js ├── routes │ ├── index.js │ ├── users.route.js │ └── auth.route.js ├── schemas │ ├── login.schema.js │ └── users.schema.js ├── utils │ ├── hash.js │ └── errorTypes.js ├── repositories │ ├── cache.repository.js │ └── users.repository.js ├── services │ ├── redis.service.js │ └── mongo.service.js ├── index.js ├── handlers │ ├── logout.handler.js │ ├── users.handler.js │ └── login.handler.js ├── models │ └── users.model.js └── server.js ├── .editorconfig ├── .env.example ├── tests └── unit │ └── routes │ └── users.route.test.js ├── docker-compose.yml ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .dbcache 3 | .env -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /src/auth/confs.js: -------------------------------------------------------------------------------- 1 | exports.LOGIN_EXPIRATION_TIME = 3600; // 1h 2 | exports.BLACKLIST_CACHE_PREFIX = 'backlistUserToken:'; 3 | exports.ALGORITHM = 'HS256'; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const userRoutes = require('./users.route'); 2 | const authRoutes = require('./auth.route'); 3 | 4 | module.exports = [ 5 | ...userRoutes, 6 | ...authRoutes, 7 | ]; 8 | -------------------------------------------------------------------------------- /src/schemas/login.schema.js: -------------------------------------------------------------------------------- 1 | const Joi = require('@hapi/joi'); 2 | 3 | module.exports = Joi.object({ 4 | email: Joi.string().email().required(), 5 | password: Joi.string().min(6).required(), 6 | }); 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MONGO_HOST=mongo 2 | MONGO_PORT=27017 3 | MONGO_DATABASE=eadapi 4 | MONGO_USER=ead_api_user 5 | MONGO_PASSWORD=ead_api_user 6 | SECRET_KEY= 7 | HOST=0.0.0.0 8 | PORT=3000 9 | REDIS_PORT=6379 10 | REDIS_HOST=redis -------------------------------------------------------------------------------- /src/utils/hash.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs'); 2 | 3 | const make = async (value) => ( 4 | bcrypt.hash(value, 10) 5 | ); 6 | 7 | const compare = (value, valueHash) => ( 8 | bcrypt.compare(value, valueHash) 9 | ); 10 | 11 | module.exports = { 12 | make, 13 | compare, 14 | }; 15 | -------------------------------------------------------------------------------- /src/repositories/cache.repository.js: -------------------------------------------------------------------------------- 1 | const Redis = require('../services/redis.service').get(); 2 | 3 | const set = (key, value, seconds) => ( 4 | Redis.set(key, value, 'EX', seconds) 5 | ); 6 | 7 | const exists = key => Redis.exists(key); 8 | 9 | const del = key => Redis.del(key); 10 | 11 | module.exports = { 12 | set, 13 | exists, 14 | del, 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/errorTypes.js: -------------------------------------------------------------------------------- 1 | const ERR_USER_NOT_FOUND = 'USER_NOT_FOUND'; 2 | const ERR_INVALID_PASSWORD = 'INVALID_PASSWORD'; 3 | const ERR_INVALID_TOKEN = 'TOKEN_ERROR'; 4 | 5 | const ERR_DUPLICATE_EMAIL = 'DUPLICATE_EMAIL'; 6 | 7 | module.exports = { 8 | ERR_USER_NOT_FOUND, 9 | ERR_INVALID_PASSWORD, 10 | ERR_INVALID_TOKEN, 11 | ERR_DUPLICATE_EMAIL, 12 | }; 13 | -------------------------------------------------------------------------------- /src/services/redis.service.js: -------------------------------------------------------------------------------- 1 | const Redis = require('ioredis'); 2 | 3 | let redis = null; 4 | 5 | exports.connect = () => { 6 | redis = new Redis({ 7 | port: process.env.REDIS_PORT, 8 | host: process.env.REDIS_HOST, 9 | }) 10 | }; 11 | 12 | exports.get = () => { 13 | if (!redis) { 14 | throw new Error('REDIS_NOT_INITIALIZED'); 15 | } 16 | 17 | return redis; 18 | }; 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv-safe').config(); 2 | require('./services/mongo.service'); 3 | require('./services/redis.service').connect(); 4 | 5 | const { start } = require('./server'); 6 | 7 | const init = async () => { 8 | // Inicializando servidor 9 | const server = await start(); 10 | console.log('Server running on %ss', server.info.uri); 11 | }; 12 | 13 | process.on('unhandledRejection', (err) => { 14 | console.log(err); 15 | process.exit(1); 16 | }); 17 | 18 | init(); 19 | -------------------------------------------------------------------------------- /src/routes/users.route.js: -------------------------------------------------------------------------------- 1 | const userHandler = require('../handlers/users.handler'); 2 | const userSchema = require('../schemas/users.schema.js'); 3 | 4 | module.exports = [ 5 | { 6 | method: 'POST', 7 | path: '/users', 8 | handler: userHandler.create, 9 | options: { 10 | validate: { 11 | payload: userSchema, 12 | }, 13 | auth: false, 14 | }, 15 | }, 16 | { 17 | method: 'GET', 18 | path: '/users', 19 | handler: userHandler.getAll, 20 | }, 21 | ]; 22 | -------------------------------------------------------------------------------- /src/auth/token.auth.js: -------------------------------------------------------------------------------- 1 | const JWT = require('jsonwebtoken'); 2 | 3 | const { ERR_INVALID_TOKEN } = require('../utils/errorTypes'); 4 | const { ALGORITHM } = require('./confs'); 5 | 6 | const generate = data => ( 7 | new Promise((resolve) => { 8 | JWT.sign(data, process.env.SECRET_KEY, { algorithm: ALGORITHM }, (err, token) => { 9 | if (err) { 10 | console.error(err); 11 | throw new Error(ERR_INVALID_TOKEN); 12 | } 13 | 14 | resolve(token); 15 | }); 16 | }) 17 | ); 18 | 19 | module.exports = { 20 | generate, 21 | }; 22 | -------------------------------------------------------------------------------- /src/routes/auth.route.js: -------------------------------------------------------------------------------- 1 | const loginHandler = require('../handlers/login.handler'); 2 | const logoutHandler = require('../handlers/logout.handler'); 3 | const loginSchema = require('../schemas/login.schema'); 4 | 5 | module.exports = [ 6 | { 7 | method: 'POST', 8 | path: '/login', 9 | handler: loginHandler.login, 10 | options: { 11 | auth: false, 12 | validate: { 13 | payload: loginSchema, 14 | }, 15 | }, 16 | }, 17 | { 18 | method: 'POST', 19 | path: '/logout', 20 | handler: logoutHandler.logout, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /src/auth/strategies/JWT.js: -------------------------------------------------------------------------------- 1 | const Cache = require('../../repositories/cache.repository'); 2 | const { BLACKLIST_CACHE_PREFIX, ALGORITHM } = require('../confs') 3 | 4 | const name = 'jwt'; 5 | 6 | const schema = 'jwt'; 7 | 8 | const options = { 9 | key: process.env.SECRET_KEY, 10 | validate: async (decoded, h) => { 11 | const unlogged = await Cache.exists(`${BLACKLIST_CACHE_PREFIX}${h.auth.token}`); 12 | return { isValid: !unlogged }; 13 | }, 14 | verifyOptions: { algorithms: [ALGORITHM] }, 15 | }; 16 | 17 | module.exports = { 18 | name, 19 | schema, 20 | options, 21 | }; 22 | -------------------------------------------------------------------------------- /src/handlers/logout.handler.js: -------------------------------------------------------------------------------- 1 | const boom = require('@hapi/boom'); 2 | const auth = require('../auth/authenticate.auth'); 3 | const userRepository = require('../repositories/users.repository'); 4 | 5 | const logout = async (req, h) => { 6 | const { credentials, token } = req.auth; 7 | try { 8 | await Promise.all([ 9 | auth.logout(token), 10 | userRepository.removeCache(credentials.data.user_id), 11 | ]); 12 | 13 | return h.response().code(200); 14 | } catch (e) { 15 | console.error(e); 16 | throw boom.badImplementation(); 17 | } 18 | }; 19 | 20 | module.exports = { 21 | logout, 22 | }; 23 | -------------------------------------------------------------------------------- /tests/unit/routes/users.route.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const sinon = require('sinon'); 3 | const Redis = require('../../../src/services/redis.service'); 4 | 5 | sinon.stub(Redis, 'get').returns({}); 6 | 7 | const { init } = require('../../../src/server'); 8 | 9 | const auth = { 10 | strategy: 'jwt', 11 | credentials: 'Bearer abc' 12 | }; 13 | 14 | let server; 15 | 16 | test.before(async t => { 17 | server = await init(); 18 | }); 19 | 20 | test('should return empty array', async (t) => { 21 | const res = await server.inject({ 22 | method: 'get', 23 | url: '/users', 24 | auth, 25 | }); 26 | t.is(res.payload, '[]'); 27 | }) -------------------------------------------------------------------------------- /src/schemas/users.schema.js: -------------------------------------------------------------------------------- 1 | const Joi = require('@hapi/joi'); 2 | 3 | module.exports = Joi.object({ 4 | name: Joi.string().min(3).required(), 5 | dateOfBirth: Joi.date().iso(), 6 | docType: Joi.string(), 7 | docNumber: Joi.string().min(3), 8 | email: Joi.string().email().required(), 9 | status: Joi.string(), 10 | password: Joi.string().min(6).required(), 11 | address: { 12 | street: Joi.string(), 13 | country: Joi.string(), 14 | state: Joi.string(), 15 | city: Joi.string(), 16 | zipcode: Joi.string(), 17 | number: Joi.string(), 18 | complement: Joi.string(), 19 | }, 20 | timestamps: Joi.any().forbidden(), 21 | }); 22 | -------------------------------------------------------------------------------- /src/services/mongo.service.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const HOST = process.env.MONGO_HOST; 4 | const PORT = process.env.MONGO_PORT; 5 | const DATABASE = process.env.MONGO_DATABASE; 6 | const USER = process.env.MONGO_USER; 7 | const PASSWORD = process.env.MONGO_PASSWORD; 8 | 9 | const uri = `mongodb://${USER}:${PASSWORD}@${HOST}:${PORT}/${DATABASE}`; 10 | 11 | const options = { 12 | useNewUrlParser: true, 13 | useUnifiedTopology: true, 14 | }; 15 | 16 | try { 17 | mongoose.connect(uri, options); 18 | } catch (error) { 19 | console.error(error); 20 | } 21 | 22 | mongoose.connection.on('error', (err) => { 23 | console.error(err); 24 | }); 25 | -------------------------------------------------------------------------------- /src/models/users.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { v4: uuidv4 } = require('uuid'); 3 | 4 | const { Schema } = mongoose; 5 | 6 | const UserSchema = new Schema({ 7 | _id: { 8 | type: String, 9 | default: uuidv4, 10 | }, 11 | name: String, 12 | dateOfBirth: Date, 13 | docType: String, 14 | docNumber: String, 15 | email: String, 16 | status: Boolean, 17 | password: String, 18 | address: { 19 | street: String, 20 | complement: String, 21 | country: String, 22 | state: String, 23 | city: String, 24 | zipcode: String, 25 | number: String, 26 | }, 27 | }, { 28 | timestamps: {}, 29 | }); 30 | 31 | module.exports = new mongoose.model('User', UserSchema); 32 | -------------------------------------------------------------------------------- /src/handlers/users.handler.js: -------------------------------------------------------------------------------- 1 | const boom = require('@hapi/boom'); 2 | const userRepository = require('../repositories/users.repository'); 3 | const hash = require('../utils/hash'); 4 | const { ERR_DUPLICATE_EMAIL } = require('../utils/errorTypes'); 5 | 6 | const create = async (req, h) => { 7 | try { 8 | const userData = req.payload; 9 | const passwordHashed = await hash.make(userData.password); 10 | 11 | userData.password = passwordHashed; 12 | 13 | const user = await userRepository.create(userData); 14 | return h.response(user).code(201); 15 | } catch (e) { 16 | switch (e.message) { 17 | case ERR_DUPLICATE_EMAIL: 18 | throw boom.badData('E-mail duplicado'); 19 | default: 20 | throw boom.badImplementation(e); 21 | } 22 | } 23 | }; 24 | 25 | const getAll = async () => []; 26 | 27 | module.exports = { 28 | create, 29 | getAll, 30 | }; 31 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mongo: 4 | image: mongo 5 | container_name: "eadapi-mongo" 6 | restart: "always" 7 | volumes: 8 | - ./.dbcache/mongo/data:/data/db 9 | ports: 10 | - "27019:27017" 11 | environment: 12 | MONGO_INITDB_ROOT_USERNAME: root 13 | MONGO_INITDB_ROOT_PASSWORD: example 14 | networks: 15 | - backend 16 | api: 17 | image: node:14.3-slim 18 | container_name: "eadapi-api" 19 | ports: 20 | - "3000:3000" 21 | working_dir: "/home/node/app" 22 | volumes: 23 | - ./:/home/node/app 24 | command: 25 | - sh 26 | - -c 27 | - | 28 | npm install 29 | npm start 30 | networks: 31 | - backend 32 | 33 | redis: 34 | image: redis:6.0-alpine 35 | container_name: "eadapi-redis" 36 | ports: 37 | - "6379:6379" 38 | networks: 39 | - backend 40 | 41 | networks: 42 | backend: 43 | driver: "bridge" 44 | -------------------------------------------------------------------------------- /src/repositories/users.repository.js: -------------------------------------------------------------------------------- 1 | const UserModel = require('../models/users.model'); 2 | const Cache = require('./cache.repository'); 3 | const { ERR_DUPLICATE_EMAIL } = require('../utils/errorTypes'); 4 | const { LOGIN_EXPIRATION_TIME } = require('../auth/confs'); 5 | 6 | const PREFIX_CACHE = 'userId:'; 7 | 8 | const create = async (userData) => { 9 | const userExists = await UserModel.exists({ email: userData.email }); 10 | 11 | if (userExists) { 12 | throw new Error(ERR_DUPLICATE_EMAIL); 13 | } 14 | 15 | const userModel = new UserModel(userData); 16 | return userModel.save(); 17 | }; 18 | 19 | const findByEmail = email => ( 20 | UserModel.findOne({ email }) 21 | ); 22 | 23 | const setCache = user => ( 24 | Cache.set(`${PREFIX_CACHE}${user.id}`, JSON.stringify(user), LOGIN_EXPIRATION_TIME) 25 | ); 26 | 27 | const removeCache = userId => ( 28 | Cache.del(`${PREFIX_CACHE}${userId}`) 29 | ); 30 | 31 | module.exports = { 32 | create, 33 | findByEmail, 34 | setCache, 35 | removeCache, 36 | }; 37 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('@hapi/hapi'); 2 | const hapiAuthJwt2 = require('hapi-auth-jwt2'); 3 | const routes = require('./routes'); 4 | const jwtStrategy = require('./auth/strategies/JWT'); 5 | 6 | const { PORT } = process.env; 7 | const { HOST } = process.env; 8 | 9 | const server = Hapi.server({ 10 | port: PORT, 11 | host: HOST, 12 | }); 13 | 14 | // Definindo rotas 15 | server.route(routes); 16 | 17 | const initializePlugins = async () => { 18 | await server.register(hapiAuthJwt2); 19 | 20 | // Definindo estratégia de autenticação 21 | server.auth.strategy(jwtStrategy.name, jwtStrategy.schema, jwtStrategy.options); 22 | server.auth.default(jwtStrategy.name); 23 | }; 24 | 25 | const start = async () => { 26 | await initializePlugins(); 27 | await server.start(); 28 | 29 | return server; 30 | }; 31 | 32 | const init = async () => { 33 | await initializePlugins(); 34 | await server.initialize(); 35 | 36 | return server; 37 | }; 38 | 39 | module.exports = { 40 | start, 41 | init, 42 | }; 43 | -------------------------------------------------------------------------------- /src/handlers/login.handler.js: -------------------------------------------------------------------------------- 1 | const boom = require('@hapi/boom'); 2 | const authenticate = require('../auth/authenticate.auth'); 3 | const userRepository = require('../repositories/users.repository'); 4 | 5 | const { 6 | ERR_INVALID_PASSWORD, 7 | ERR_INVALID_TOKEN, 8 | ERR_USER_NOT_FOUND, 9 | } = require('../utils/errorTypes'); 10 | 11 | const login = async (req, h) => { 12 | const { email, password } = req.payload; 13 | 14 | try { 15 | const { user, token } = await authenticate.login(email, password); 16 | 17 | await userRepository.setCache(user); 18 | 19 | return h.response({ token }).code(200); 20 | } catch (e) { 21 | switch (e.message) { 22 | case ERR_INVALID_PASSWORD: 23 | throw boom.notFound('E-mail ou senha inválido'); 24 | case ERR_INVALID_TOKEN: 25 | throw boom.badImplementation('Erro ao gerar token'); 26 | case ERR_USER_NOT_FOUND: 27 | throw boom.notFound('E-mail ou senha inválido'); 28 | default: 29 | throw boom.badImplementation(e); 30 | } 31 | } 32 | }; 33 | 34 | module.exports = { 35 | login, 36 | }; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Ayrton Teshima 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/auth/authenticate.auth.js: -------------------------------------------------------------------------------- 1 | const Token = require('./token.auth'); 2 | const Cache = require('../repositories/cache.repository'); 3 | const { findByEmail } = require('../repositories/users.repository'); 4 | const { LOGIN_EXPIRATION_TIME, BLACKLIST_CACHE_PREFIX } = require('./confs'); 5 | 6 | const hash = require('../utils/hash'); 7 | 8 | const { 9 | ERR_USER_NOT_FOUND, 10 | ERR_INVALID_PASSWORD, 11 | } = require('../utils/errorTypes'); 12 | 13 | const login = async (email, password) => { 14 | const user = await findByEmail(email); 15 | 16 | if (!user) { 17 | throw new Error(ERR_USER_NOT_FOUND); 18 | } 19 | 20 | const passwordOk = await hash.compare(password, user.password); 21 | 22 | if (!passwordOk) { 23 | throw new Error(ERR_INVALID_PASSWORD); 24 | } 25 | 26 | const JWTData = { 27 | exp: Math.floor(Date.now() / 1000) + LOGIN_EXPIRATION_TIME, 28 | sub: user.id, 29 | iss: 'ead-api', 30 | data: { 31 | user_id: user.id, 32 | }, 33 | }; 34 | 35 | const token = await Token.generate(JWTData); 36 | 37 | return { user, token }; 38 | }; 39 | 40 | const logout = token => ( 41 | Cache.set(`${BLACKLIST_CACHE_PREFIX}${token}`, 1, LOGIN_EXPIRATION_TIME) 42 | ); 43 | 44 | module.exports = { 45 | login, 46 | logout, 47 | }; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ead-api", 3 | "version": "0.0.1", 4 | "description": "API da plataforma de ensino a distância", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "ava -f tests/**/*.test.js", 8 | "start": "nodemon src/index", 9 | "eslint": "eslint ./src/**/*.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/programadorabordo/ead-api.git" 14 | }, 15 | "keywords": [ 16 | "plataforma", 17 | "EAD", 18 | "programador", 19 | "a", 20 | "bordo" 21 | ], 22 | "author": "Programador a Bordo, Ayrton Teshima", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/programadorabordo/ead-api/issues" 26 | }, 27 | "homepage": "https://github.com/programadorabordo/ead-api#readme", 28 | "devDependencies": { 29 | "ava": "^3.8.2", 30 | "eslint": "^7.1.0", 31 | "eslint-config-airbnb-base": "^14.1.0", 32 | "eslint-plugin-import": "^2.20.2", 33 | "nodemon": "^2.0.4", 34 | "sinon": "^9.0.2" 35 | }, 36 | "dependencies": { 37 | "@hapi/boom": "^9.1.0", 38 | "@hapi/hapi": "^19.1.1", 39 | "@hapi/joi": "^17.1.1", 40 | "bcryptjs": "^2.4.3", 41 | "dotenv-safe": "^8.2.0", 42 | "hapi-auth-jwt2": "^10.1.0", 43 | "ioredis": "^4.17.1", 44 | "jsonwebtoken": "^8.5.1", 45 | "mongoose": "^5.9.16", 46 | "uuid": "^8.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plataforma EAD - Programador a Bordo 2 | 3 | API RESTFul da plataforma de ensino a distância. Projeto em Node/JS seguindo boas práticas e de fácil integração. 4 | 5 | ## Dependências 6 | * Node 14.3 e NPM (apenas se desejar rodar fora do container) 7 | * Docker 8 | * Docker Compose 9 | 10 | ## Configurando e rodando local 11 | Renomeie o arquivo `.env.example` para `.env`, gere uma SECRET_KEY e adicione o valor a variável `SECRET_KEY` no arquivo. 12 | Para rodar local, você pode por qualquer valor no SECRET_KEY ou gerar uma de forma mais segura com o comando: 13 | ``` 14 | node -e "console.log(require('crypto').randomBytes(256).toString('base64'));" 15 | ``` 16 | ### Rode os containers 17 | ``` 18 | docker-compose up -d 19 | ``` 20 | 21 | ### Configure o MongoDB 22 | Para rodar o projeto local, é necessário criar o usuário do banco de dados `eadapi` que utilizamos no sistema. 23 | Acesse o container docker do mongo pelo terminal: 24 | ``` 25 | docker exec -it eadapi-mongo /bin/bash 26 | ``` 27 | 28 | Axecute o seguinte comando dentro do container: 29 | 30 | ``` 31 | mongo -uroot -pexample <