├── .editorconfig ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── Insomnia.json ├── Procfile ├── README.MD ├── jest.config.js ├── jest.setup.js ├── package.json ├── src ├── app.js ├── app │ ├── controllers │ │ ├── UserController.js │ │ └── index.js │ ├── middlewares │ │ ├── Auth.js │ │ ├── Check.js │ │ └── Token.js │ └── models │ │ └── User.js ├── routes.js └── server.js └── tests └── api.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = false 8 | insert_final_newline = false 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SECRET_JWT=XXX 2 | MONGO_CONN=mongodb+srv://:@cluster0-okpdn.gcp.mongodb.net/test?retryWrites=true&w=majority -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true 5 | }, 6 | extends: ['airbnb-base'], 7 | globals: { 8 | Atomics: 'readonly', 9 | SharedArrayBuffer: 'readonly' 10 | }, 11 | parserOptions: { 12 | ecmaVersion: 2018, 13 | sourceType: 'module' 14 | }, 15 | rules: { 16 | 'comma-dangle': ['error', 'never'], 17 | 'class-methods-use-this': 0, 18 | 'no-underscore-dangle': 0, 19 | 'no-undef': 0, 20 | 'consistent-return': 0, 21 | 'func-names': 0, 22 | 'no-param-reassign': 0, 23 | 'no-console': 0, 24 | 'no-const-assign': 0, 25 | 'no-plusplus': 0, 26 | 'linebreak-style': 0, 27 | radix: 0, 28 | eqeqeq: 0, 29 | camelcase: 0, 30 | 'no-unused-vars': ['error', { argsIgnorePattern: 'next' }] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .env 4 | .env.test 5 | 6 | yarn.lock 7 | package.json.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /Insomnia.json: -------------------------------------------------------------------------------- 1 | { 2 | "_type": "export", 3 | "__export_format": 4, 4 | "__export_date": "2020-08-28T17:43:23.280Z", 5 | "__export_source": "insomnia.desktop.app:v2020.3.3", 6 | "resources": [ 7 | { 8 | "_id": "req_09ffd1fdffd64ed997c42b85ab535c85", 9 | "authentication": {}, 10 | "body": { 11 | "mimeType": "application/json", 12 | "text": "{\n\t\"name\": \"Caio Agiani\",\n\t\"email\": \"caio.agiani14@gmail.com\",\n\t\"password\": \"123\",\n\t\"telefones\": [\n\t\t{\n\t\t\t\"numero\": \"999865802\",\n\t\t\t\"ddd\": \"11\"\n\t\t},\n\t\t{\n\t\t\t\"numero\": \"999865802\",\n\t\t\t\"ddd\": \"11\"\n\t\t}\n\t]\n}" 13 | }, 14 | "created": 1589904350040, 15 | "description": "", 16 | "headers": [ 17 | { 18 | "id": "pair_91070e45cbe549ee9324acfcb71cde5d", 19 | "name": "Content-Type", 20 | "value": "application/json" 21 | } 22 | ], 23 | "isPrivate": false, 24 | "metaSortKey": -1589904353138, 25 | "method": "POST", 26 | "modified": 1598636544852, 27 | "name": "Criar usuário", 28 | "parameters": [], 29 | "parentId": "fld_c1449faa3ed84f1ea8ee1ce0d7177d31", 30 | "settingDisableRenderRequestBody": false, 31 | "settingEncodeUrl": true, 32 | "settingFollowRedirects": "global", 33 | "settingRebuildPath": true, 34 | "settingSendCookies": true, 35 | "settingStoreCookies": true, 36 | "url": "{{ deployurl }}/user/create", 37 | "_type": "request" 38 | }, 39 | { 40 | "_id": "fld_c1449faa3ed84f1ea8ee1ce0d7177d31", 41 | "created": 1589904356236, 42 | "description": "", 43 | "environment": {}, 44 | "environmentPropertyOrder": null, 45 | "metaSortKey": -1589904356236, 46 | "modified": 1589904356236, 47 | "name": "User", 48 | "parentId": "wrk_00cebd9c46be4b4e8cb258c5c376d38a", 49 | "_type": "request_group" 50 | }, 51 | { 52 | "_id": "wrk_00cebd9c46be4b4e8cb258c5c376d38a", 53 | "created": 1589904343262, 54 | "description": "", 55 | "modified": 1589904343262, 56 | "name": "Backend - SKY", 57 | "parentId": null, 58 | "scope": null, 59 | "_type": "workspace" 60 | }, 61 | { 62 | "_id": "req_89958d273e094ac2b3482b72103e1fc9", 63 | "authentication": {}, 64 | "body": { 65 | "mimeType": "application/json", 66 | "text": "{\n\t\"email\": \"caio.agiani14@gmail.com\",\n\t\"password\": \"123\"\n}" 67 | }, 68 | "created": 1589908790626, 69 | "description": "", 70 | "headers": [ 71 | { 72 | "id": "pair_2aa2d0cc02ff45b58c2c5c3e68bec085", 73 | "name": "Content-Type", 74 | "value": "application/json" 75 | } 76 | ], 77 | "isPrivate": false, 78 | "metaSortKey": -1589532989403.5, 79 | "method": "POST", 80 | "modified": 1598636523904, 81 | "name": "Login", 82 | "parameters": [], 83 | "parentId": "fld_c1449faa3ed84f1ea8ee1ce0d7177d31", 84 | "settingDisableRenderRequestBody": false, 85 | "settingEncodeUrl": true, 86 | "settingFollowRedirects": "global", 87 | "settingRebuildPath": true, 88 | "settingSendCookies": true, 89 | "settingStoreCookies": true, 90 | "url": "{{ deployurl }}/login", 91 | "_type": "request" 92 | }, 93 | { 94 | "_id": "req_79f010b903914017b48822c188bc525e", 95 | "authentication": {}, 96 | "body": {}, 97 | "created": 1589908456100, 98 | "description": "", 99 | "headers": [ 100 | { 101 | "description": "", 102 | "id": "pair_61ed25528ec2470c9bf61b390f4e341a", 103 | "name": "authentication", 104 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlYzRiM2E5OTViOTA4NTAxOGFlYzYwNSIsIm5hbWUiOiJDYWlvIEFnaWFuaSIsImVtYWlsIjoiY2Fpby5hZ2lhbmkxNEBnbWFpbC5jb20iLCJleHBpcmVzSW4iOjE1OTg2MzgzMjUzMDMsImlhdCI6MTU5ODYzNjUyNX0.2Rm8h6tjoe15bMwl9a98BMnvspBl2wWeGrowtMOvgWY" 105 | } 106 | ], 107 | "isPrivate": false, 108 | "metaSortKey": -1589532989353.5, 109 | "method": "GET", 110 | "modified": 1598636534153, 111 | "name": "Listar usuário", 112 | "parameters": [], 113 | "parentId": "fld_c1449faa3ed84f1ea8ee1ce0d7177d31", 114 | "settingDisableRenderRequestBody": false, 115 | "settingEncodeUrl": true, 116 | "settingFollowRedirects": "global", 117 | "settingRebuildPath": true, 118 | "settingSendCookies": true, 119 | "settingStoreCookies": true, 120 | "url": "{{ deployurl }}/user/5ec4b3a995b9085018aec605", 121 | "_type": "request" 122 | }, 123 | { 124 | "_id": "env_32c7cae8c8cfb9f290798d048c9bef83b9426071", 125 | "color": null, 126 | "created": 1589904343498, 127 | "data": { 128 | "baseUrl": "localhost:3333", 129 | "deployurl": "https://apirestful-sky.herokuapp.com" 130 | }, 131 | "dataPropertyOrder": { "&": ["baseUrl", "deployurl"] }, 132 | "isPrivate": false, 133 | "metaSortKey": 1589904343498, 134 | "modified": 1598636271282, 135 | "name": "Base Environment", 136 | "parentId": "wrk_00cebd9c46be4b4e8cb258c5c376d38a", 137 | "_type": "environment" 138 | }, 139 | { 140 | "_id": "jar_32c7cae8c8cfb9f290798d048c9bef83b9426071", 141 | "cookies": [], 142 | "created": 1589904343502, 143 | "modified": 1589904343502, 144 | "name": "Default Jar", 145 | "parentId": "wrk_00cebd9c46be4b4e8cb258c5c376d38a", 146 | "_type": "cookie_jar" 147 | }, 148 | { 149 | "_id": "spc_ace49270e1e440a7af78bef1a0c58756", 150 | "contentType": "yaml", 151 | "contents": "", 152 | "created": 1598636231927, 153 | "fileName": "Backend - SKY", 154 | "modified": 1598636231927, 155 | "parentId": "wrk_00cebd9c46be4b4e8cb258c5c376d38a", 156 | "_type": "api_spec" 157 | } 158 | ] 159 | } 160 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start 2 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 |

2 | API RESTful 3 |

4 | 5 |

API RESTful - NoSQL MongoDB

6 | 7 |
8 | 9 | [![Status](https://img.shields.io/badge/status-active-success.svg)]() 10 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) 11 | 12 |
13 | 14 | ## Apresentação: 15 | 16 | Projeto BackEnd: API RESTful seguindo conceito **MVC** utilizando Stack **NodeJS**, padronizado o código com **Eslint** e **Prettier**. 17 | 18 | Pacotes principais: **Express** responsável pela criação de rotas, Middleware **JWT** para persistência do token por 30minutos, mantendo a segurança e credêncial do usuário, ORM **Mongoose** para conexão com banco de dados NoSQL (**MongoDB**) e por fim, **Jest** para realização de tests. 19 | 20 | Rotas mapeada do arquivo exportado Insominia: `Insomnia.json` 21 | 22 | ## Requisitos 23 | 24 | #### Sign up 25 | 26 | - [x] Este endpoint deverá receber um usuário com os seguintes campos: nome, 27 | email, senha e uma lista de objetos telefone. 28 | 29 | ```java 30 | { 31 | "nome": "string", 32 | "email": "string", 33 | "senha": "senha", 34 | "telefones": [ 35 | { 36 | "numero": "123456789", 37 | "ddd": "11" 38 | } 39 | ] 40 | } 41 | ``` 42 | 43 | - [x] Usar status codes de acordo 44 | - [x] Em caso de sucesso irá retornar um usuário mais os campos: 45 | 46 | * **id**: id do usuário (pode ser o próprio gerado pelo banco, porém seria interessante 47 | se fosse um GUID 48 | * **data_criacao**: data da criação do usuário 49 | * **data_atualizacao**: data da última atualização do usuário 50 | * **ultimo_login**: data do último login (no caso da criação, será a mesma que a 51 | criação) 52 | * **token**: token de acesso da API (pode ser um GUID ou um JWT) 53 | 54 | - [x] Caso o e-mail já exista, deverá retornar erro com a mensagem "E-mail já 55 | existente". 56 | - [x] Token deverá ser persistido junto com o usuário 57 | 58 | #### Sign in 59 | 60 | - [x] Este endpoint irá receber um objeto com e-mail e senha. 61 | - [x] Caso o e-mail exista e a senha seja a mesma que a senha persistida, retornar 62 | igual ao endpoint de `sign_up`. 63 | - [x] Caso o e-mail não exista, retornar erro com status apropriado mais a mensagem 64 | "Usuário e/ou senha inválidos" 65 | - [x] Caso o e-mail exista mas a senha não bata, retornar o status apropriado 401 66 | mais a mensagem "Usuário e/ou senha inválidos" 67 | 68 | #### Buscar usuário 69 | 70 | - [x] Chamadas para este endpoint devem conter um header na requisição de 71 | Authentication com o valor "Bearer {token}" onde {token} é o valor do token 72 | passado na criação ou sign in de um usuário. 73 | - [x] Caso o token não exista, retornar erro com status apropriado com a mensagem 74 | "Não autorizado". 75 | - [x] Caso o token exista, buscar o usuário pelo user_id passado no path e comparar 76 | se o token no modelo é igual ao token passado no header. 77 | - [x] Caso não seja o mesmo token, retornar erro com status apropriado e mensagem 78 | "Não autorizado" 79 | - [x] Caso seja o mesmo token, verificar se o último login foi a MENOS que 30 80 | minutos atrás. 81 | - [x] Caso não seja a MENOS que 30 minutos atrás, retornar erro com status 82 | apropriado com mensagem "Sessão inválida". 83 | - [x] Caso tudo esteja ok, retornar o usuário. 84 | 85 | ## Instação: 86 | 87 | - Variável de ambiente `.env.example`: 88 | 89 | ```java 90 | SECRET_JWT=XXX 91 | MONGO_CONN=mongodb+srv://:@cluster0-okpdn.gcp.mongodb.net/api 92 | ``` 93 | 94 | - Instalar dependências: `yarn install` ou `npm install` 95 | - Iniciar aplicação em modo de desenvolvimento: `yarn dev:start` ou `npm run dev:start`. 96 | - Deploy da aplicação: `yarn start` 97 | - Tests Jest: `yarn test` 98 | 99 | ## Rotas 100 | 101 | **POST** `localhost:3333/user/create` 102 | 103 | ```java 104 | { 105 | "name": "Caio Agiani", 106 | "email": "caio.agiani14@gmail.com", 107 | "password": "123", 108 | "telefones": [ 109 | { 110 | "numero": "999865802", 111 | "ddd": "11" 112 | }, 113 | { 114 | "numero": "999865802", 115 | "ddd": "11" 116 | } 117 | ] 118 | } 119 | ``` 120 | 121 | **POST** `localhost:3333/login` 122 | 123 | ```java 124 | { 125 | "email": "caio.agiani14@gmail.com", 126 | "password": "123" 127 | } 128 | ``` 129 | 130 | **GET** `localhost:3333/user/:user_id` 131 | 132 | Observação importante: rota `/user/:user_id` é necessário passsar no **header** o parâmetro **authentication** contendo o Token Bearer coletado na rota `login` 133 | 134 | ## Test 135 | 136 | ```javascript 137 | caio-agiani in api-restful on  master [!] took 2s ❯ yarn test 138 | yarn run v1.22.5 139 | $ jest --setupFiles dotenv/config --detectOpenHandles --forceExit 140 | PASS tests/api.test.js 141 | Authentication 142 | ✓ should create session authentication (1233 ms) 143 | Login 144 | ✓ should create user session (131 ms) 145 | User 146 | ✓ should list user by id (9 ms) 147 | 148 | Test Suites: 1 passed, 1 total 149 | Tests: 3 passed, 3 total 150 | Snapshots: 0 total 151 | Time: 2.136 s 152 | Ran all test suites. 153 | Done in 2.58s. 154 | ``` 155 | 156 | ## Contato 157 | 158 | - [LinkedIn](https://www.linkedin.com/in/caioagiani/) 159 | - caio.agiani14@gmail.com 160 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | bail: true, 10 | 11 | // The directory where Jest should store its cached dependency information 12 | // cacheDirectory: "C:\\Users\\caioh\\AppData\\Local\\Temp\\jest", 13 | 14 | // Automatically clear mock calls and instances between every test 15 | clearMocks: true, 16 | 17 | // Indicates whether the coverage information should be collected while executing the test 18 | // collectCoverage: false, 19 | 20 | // An array of glob patterns indicating a set of files for which coverage information should be collected 21 | // collectCoverageFrom: undefined, 22 | 23 | // The directory where Jest should output its coverage files 24 | // coverageDirectory: undefined, 25 | 26 | // An array of regexp pattern strings used to skip coverage collection 27 | // coveragePathIgnorePatterns: [ 28 | // "\\\\node_modules\\\\" 29 | // ], 30 | 31 | // A list of reporter names that Jest uses when writing coverage reports 32 | // coverageReporters: [ 33 | // "json", 34 | // "text", 35 | // "lcov", 36 | // "clover" 37 | // ], 38 | 39 | // An object that configures minimum threshold enforcement for coverage results 40 | // coverageThreshold: undefined, 41 | 42 | // A path to a custom dependency extractor 43 | // dependencyExtractor: undefined, 44 | 45 | // Make calling deprecated APIs throw helpful error messages 46 | // errorOnDeprecated: false, 47 | 48 | // Force coverage collection from ignored files using an array of glob patterns 49 | // forceCoverageMatch: [], 50 | 51 | // A path to a module which exports an async function that is triggered once before all test suites 52 | // globalSetup: undefined, 53 | 54 | // A path to a module which exports an async function that is triggered once after all test suites 55 | // globalTeardown: undefined, 56 | 57 | // A set of global variables that need to be available in all test environments 58 | // globals: {}, 59 | 60 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 61 | // maxWorkers: "50%", 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | // moduleFileExtensions: [ 70 | // "js", 71 | // "json", 72 | // "jsx", 73 | // "ts", 74 | // "tsx", 75 | // "node" 76 | // ], 77 | 78 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 79 | // moduleNameMapper: {}, 80 | 81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 82 | // modulePathIgnorePatterns: [], 83 | 84 | // Activates notifications for test results 85 | // notify: false, 86 | 87 | // An enum that specifies notification mode. Requires { notify: true } 88 | // notifyMode: "failure-change", 89 | 90 | // A preset that is used as a base for Jest's configuration 91 | // preset: undefined, 92 | 93 | // Run tests from one or more projects 94 | // projects: undefined, 95 | 96 | // Use this configuration option to add custom reporters to Jest 97 | // reporters: undefined, 98 | 99 | // Automatically reset mock state between every test 100 | // resetMocks: false, 101 | 102 | // Reset the module registry before running each individual test 103 | // resetModules: false, 104 | 105 | // A path to a custom resolver 106 | // resolver: undefined, 107 | 108 | // Automatically restore mock state between every test 109 | // restoreMocks: false, 110 | 111 | // The root directory that Jest should scan for tests and modules within 112 | // rootDir: undefined, 113 | 114 | // A list of paths to directories that Jest should use to search for files in 115 | // roots: [ 116 | // "" 117 | // ], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | setupFilesAfterEnv: ['./jest.setup.js'], 127 | 128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 129 | // snapshotSerializers: [], 130 | 131 | // The test environment that will be used for testing 132 | testEnvironment: 'node', 133 | 134 | // Options that will be passed to the testEnvironment 135 | // testEnvironmentOptions: {}, 136 | 137 | // Adds a location field to test results 138 | // testLocationInResults: false, 139 | 140 | // The glob patterns Jest uses to detect test files 141 | testMatch: ['**/tests/**/*.test.js?(x)'], 142 | 143 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 144 | // testPathIgnorePatterns: [ 145 | // "\\\\node_modules\\\\" 146 | // ], 147 | 148 | // The regexp pattern or array of patterns that Jest uses to detect test files 149 | // testRegex: [], 150 | 151 | // This option allows the use of a custom results processor 152 | // testResultsProcessor: undefined, 153 | 154 | // This option allows use of a custom test runner 155 | // testRunner: "jasmine2", 156 | 157 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 158 | // testURL: "http://localhost", 159 | 160 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 161 | // timers: "real", 162 | 163 | // A map from regular expressions to paths to transformers 164 | // transform: undefined, 165 | 166 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 167 | // transformIgnorePatterns: [ 168 | // "\\\\node_modules\\\\" 169 | // ], 170 | 171 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 172 | // unmockedModulePathPatterns: undefined, 173 | 174 | // Indicates whether each individual test should be reported during the run 175 | // verbose: undefined, 176 | 177 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 178 | // watchPathIgnorePatterns: [], 179 | 180 | // Whether to use watchman for file crawling 181 | // watchman: true, 182 | }; 183 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(30000); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-sky", 3 | "version": "0.0.1", 4 | "description": "API RESTful SKY", 5 | "main": "src/server.js", 6 | "license": "MIT", 7 | "keywords": [ 8 | "api", 9 | "restful", 10 | "sky" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/caioagiani" 15 | }, 16 | "author": { 17 | "name": "Caio Agiani", 18 | "email": "caio.agiani14@gmail.com", 19 | "url": "https://www.linkedin.com/in/caioagiani" 20 | }, 21 | "scripts": { 22 | "start": "node src/server.js", 23 | "dev:start": "nodemon -r dotenv/config src/server.js --ignore tests", 24 | "test": "jest --setupFiles dotenv/config --detectOpenHandles --forceExit", 25 | "test:clear": "jest --clearCache" 26 | }, 27 | "dependencies": { 28 | "bcryptjs": "^2.4.3", 29 | "cors": "^2.8.5", 30 | "express": "^4.17.1", 31 | "jsonwebtoken": "^8.5.1", 32 | "mongoose": "^5.9.15", 33 | "yup": "^0.29.3" 34 | }, 35 | "devDependencies": { 36 | "dotenv": "^8.2.0", 37 | "eslint": "^7.1.0", 38 | "eslint-config-airbnb-base": "^14.1.0", 39 | "eslint-config-prettier": "^6.11.0", 40 | "eslint-plugin-import": "^2.20.2", 41 | "eslint-plugin-prettier": "^3.1.3", 42 | "jest": "^26.0.1", 43 | "nodemon": "^2.0.4", 44 | "supertest": "^4.0.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const { connect } = require('mongoose'); 2 | const express = require('express'); 3 | const cors = require('cors'); 4 | 5 | const routes = require('./routes'); 6 | 7 | const server = express(); 8 | 9 | server.use(cors()); 10 | server.use(express.json()); 11 | server.use(express.urlencoded({ extended: true })); 12 | server.use(routes); 13 | 14 | (async () => { 15 | await connect(process.env.MONGO_CONN, { 16 | useNewUrlParser: true, 17 | useUnifiedTopology: true, 18 | useCreateIndex: true, 19 | useFindAndModify: false 20 | }); 21 | })(); 22 | 23 | module.exports = server; 24 | -------------------------------------------------------------------------------- /src/app/controllers/UserController.js: -------------------------------------------------------------------------------- 1 | const bcryptjs = require('bcryptjs'); 2 | const { createToken } = require('../middlewares/Token'); 3 | 4 | const User = require('../models/User'); 5 | 6 | module.exports = { 7 | async index(req, res) { 8 | const { tokenId } = req; 9 | const { user_id } = req.params; 10 | 11 | if (user_id != tokenId) { 12 | return res.status(401).json({ mensagem: 'Não autorizado.' }); 13 | } 14 | 15 | const user = await User.findById({ _id: user_id }); 16 | 17 | user.senha = undefined; 18 | 19 | return res.status(200).json(user); 20 | }, 21 | 22 | async store(req, res) { 23 | const { email } = req.body; 24 | 25 | const checkEmail = await User.findOne({ email }); 26 | 27 | if (checkEmail) { 28 | return res.status(400).json({ mensagem: 'E-mail já existente.' }); 29 | } 30 | 31 | const user = await User.create(req.body); 32 | 33 | user.senha = undefined; 34 | 35 | return res.status(200).json(user); 36 | }, 37 | 38 | async show(req, res) { 39 | const { email, senha } = req.body; 40 | 41 | const user = await User.findOne({ email }, '+senha'); 42 | 43 | if (!user) { 44 | return res 45 | .status(400) 46 | .json({ mensagem: 'Usuário e/ou senha inválidos.' }); 47 | } 48 | 49 | const checkPass = bcryptjs.compareSync(senha, user.senha); 50 | 51 | if (!checkPass) { 52 | return res 53 | .status(400) 54 | .json({ mensagem: 'Usuário e/ou senha inválidos.' }); 55 | } 56 | 57 | const token = createToken({ 58 | id: user.id, 59 | nome: user.nome, 60 | email 61 | }); 62 | 63 | await User.findOneAndUpdate( 64 | { _id: user._id }, 65 | { token, data_ultima_atualizacao: Date.now() } 66 | ); 67 | 68 | user.senha = undefined; 69 | user.token = token; 70 | 71 | return res.status(200).json(user); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/app/controllers/index.js: -------------------------------------------------------------------------------- 1 | module.exports.UserController = require('./UserController'); 2 | module.exports.AuthMiddleware = require('../middlewares/Auth'); 3 | -------------------------------------------------------------------------------- /src/app/middlewares/Auth.js: -------------------------------------------------------------------------------- 1 | const { verify } = require('jsonwebtoken'); 2 | 3 | module.exports = (req, res, next) => { 4 | const { authentication } = req.headers; 5 | 6 | if (!authentication) { 7 | return res.status(401).json({ mensagem: 'Não autorizado.' }); 8 | } 9 | 10 | const [, token] = authentication.split(' '); 11 | 12 | try { 13 | verify(token, process.env.SECRET_JWT, (err, decoded) => { 14 | if (err) { 15 | return res.status(401).json({ mensagem: 'Não autorizado.' }); 16 | } 17 | 18 | if (decoded.expiresIn < Date.now()) { 19 | return res.status(401).json({ mensagem: 'Sessão inválida.' }); 20 | } 21 | 22 | req.tokenId = decoded.id; 23 | req.tokenName = decoded.name; 24 | req.tokenEmail = decoded.email; 25 | 26 | return next(); 27 | }); 28 | } catch (error) { 29 | return res.status(401).json({ mensagem: 'Não autorizado.' }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/middlewares/Check.js: -------------------------------------------------------------------------------- 1 | const Yup = require('yup'); 2 | 3 | module.exports = { 4 | async SingIn(req, res, next) { 5 | try { 6 | const schema = Yup.object().shape({ 7 | email: Yup.string().email().required(), 8 | senha: Yup.string().required() 9 | }); 10 | 11 | await schema.validate(req.body, { abortEarly: false }); 12 | 13 | return next(); 14 | } catch (error) { 15 | return res.json({ error }); 16 | } 17 | }, 18 | async SingUp(req, res, next) { 19 | try { 20 | const schema = Yup.object().shape({ 21 | nome: Yup.string().required(), 22 | email: Yup.string().email().required(), 23 | senha: Yup.string().required(), 24 | telefones: Yup.array( 25 | Yup.object().shape({ 26 | numero: Yup.string().required(), 27 | ddd: Yup.string().required() 28 | }) 29 | ) 30 | }); 31 | 32 | await schema.validate(req.body, { abortEarly: false }); 33 | 34 | return next(); 35 | } catch (error) { 36 | return res.json({ error }); 37 | } 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/app/middlewares/Token.js: -------------------------------------------------------------------------------- 1 | const { sign } = require('jsonwebtoken'); 2 | 3 | /** 4 | * Generate token with timestamp +30minutes 5 | * 6 | * @param {*} [params={}] 7 | * @returns JWT 8 | */ 9 | const createToken = (params = {}) => { 10 | const expiresSet = new Date(Date.now()); 11 | expiresSet.setMinutes(expiresSet.getMinutes() + 30); 12 | 13 | return sign( 14 | { ...params, expiresIn: parseInt(expiresSet.getTime()) }, 15 | process.env.SECRET_JWT 16 | ); 17 | }; 18 | 19 | module.exports = { createToken }; 20 | -------------------------------------------------------------------------------- /src/app/models/User.js: -------------------------------------------------------------------------------- 1 | const { Schema, model } = require('mongoose'); 2 | const { hashSync } = require('bcryptjs'); 3 | 4 | const UserSchema = new Schema( 5 | { 6 | nome: { 7 | type: String, 8 | required: true 9 | }, 10 | email: { 11 | type: String, 12 | unique: true, 13 | required: true, 14 | lowercase: true 15 | }, 16 | telefones: [ 17 | { 18 | numero: String, 19 | ddd: String 20 | } 21 | ], 22 | senha: { 23 | type: String, 24 | required: true, 25 | select: false 26 | }, 27 | data_ultima_atualizacao: { 28 | type: Date, 29 | default: Date.now() 30 | }, 31 | token: { 32 | type: String, 33 | default: '' 34 | } 35 | }, 36 | { 37 | timestamps: { createdAt: 'data_criacao', updatedAt: 'data_atualizacao' } 38 | } 39 | ); 40 | 41 | UserSchema.pre('save', function () { 42 | this.senha = hashSync(this.senha, 1); 43 | }); 44 | 45 | UserSchema.pre('updateOne', async function () { 46 | const pass = this.getUpdate().senha; 47 | 48 | if (pass) this.getUpdate().senha = hashSync(pass, 10); 49 | }); 50 | 51 | module.exports = model('User', UserSchema); 52 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | const routes = require('express').Router(); 2 | 3 | const { 4 | UserController, 5 | AuthMiddleware 6 | } = require('./app/controllers'); 7 | 8 | const { SingIn, SingUp } = require('./app/middlewares/Check'); 9 | 10 | routes 11 | .post('/login', SingIn, UserController.show) 12 | .post('/user/create', SingUp, UserController.store) 13 | .get('/user/:user_id', AuthMiddleware, UserController.index); 14 | 15 | module.exports = routes; 16 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @author @Caio Agiani 3 | * @description API RESTful - NoSQL MongoDB 4 | * @website https://www.linkedin.com/in/caioagiani/ 5 | */ 6 | 7 | const app = require('./app'); 8 | 9 | app.listen(process.env.PORT || 3333, () => { 10 | console.log({ status: 'on', company: 'Sky', banco: 'mongodb' }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/api.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../src/app'); 3 | 4 | describe('Create', () => { 5 | it('should create user session', async (done) => { 6 | const response = await request(app) 7 | .post('/user/create') 8 | .send({ 9 | nome: 'Caio Agiani', 10 | email: `caio.agiani${Math.floor(Math.random() * 9999) + 1}@gmail.com`, 11 | senha: '123123123', 12 | telefones: [ 13 | { 14 | numero: '999865802', 15 | ddd: '11' 16 | } 17 | ] 18 | }); 19 | 20 | expect(response.status).toBe(200); 21 | done(); 22 | }); 23 | }); 24 | 25 | describe('Authentication', () => { 26 | it('should create session authentication', async (done) => { 27 | const response = await request(app).post('/login').send({ 28 | email: 'caio.agiani14@gmail.com', 29 | senha: '123' 30 | }); 31 | 32 | expect(response.status).toBe(200); 33 | done(); 34 | }); 35 | }); 36 | 37 | describe('User', () => { 38 | it('should list user by id', async (done) => { 39 | const response = await request(app) 40 | .get('/user/5f877dc25d9f7b5c08f77c16') 41 | .send(); 42 | 43 | expect(response.status).toBe(401); 44 | done(); 45 | }); 46 | }); 47 | --------------------------------------------------------------------------------