├── .env_example ├── .gitignore ├── .sequelizerc ├── LICENSE ├── MISC_FILES ├── Insomnia_2020-04-30.json ├── keybindings.json └── settings.json ├── README.md ├── nodemon.json ├── package-lock.json ├── package.json ├── setup-linux-mac.sh ├── setup-windows.bat ├── src ├── app.js ├── config │ ├── appConfig.js │ ├── database.js │ └── multerConfig.js ├── controllers │ ├── AlunoController.js │ ├── FotoController.js │ ├── HomeController.js │ ├── TokenController.js │ └── UserController.js ├── database │ ├── index.js │ ├── migrations │ │ ├── 20191111123839-alunos.js │ │ ├── 20191111181936-users.js │ │ ├── 20191113141734-mudar-email-aluno-unique.js │ │ └── 20191115165102-criar-tabela-de-foto-do-aluno.js │ └── seeds │ │ └── 20191113015433-criar-usuarios.js ├── middlewares │ └── loginRequired.js ├── models │ ├── Aluno.js │ ├── Foto.js │ └── User.js ├── routes │ ├── alunoRoutes.js │ ├── fotoRoutes.js │ ├── homeRoutes.js │ ├── tokenRoutes.js │ └── userRoutes.js └── server.js └── uploads └── images └── images_here.txt /.env_example: -------------------------------------------------------------------------------- 1 | DATABASE='' 2 | DATABASE_HOST='127.0.0.1' 3 | DATABASE_PORT=3306 4 | DATABASE_USERNAME='' 5 | DATABASE_PASSWORD='' 6 | 7 | TOKEN_SECRET='sua_secret_key_aqui' 8 | TOKEN_EXPIRATION=7d 9 | 10 | APP_PORT=3001 11 | APP_URL=http://localhost:3001 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | *.jpg 4 | *.png 5 | *.gif 6 | junk.js 7 | pull-server.sh 8 | sync.sh 9 | db.sqlite -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | module.exports = { 4 | config: resolve(__dirname, 'src', 'config', 'database.js'), 5 | 'models-path': resolve(__dirname, 'src', 'models'), 6 | 'migrations-path': resolve(__dirname, 'src', 'database', 'migrations'), 7 | 'seeders-path': resolve(__dirname, 'src', 'database','seeds'), 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | ----------- 3 | 4 | Copyright (c) 2019 Luiz Otávio Miranda (https://www.otaviomiranda.com.br/) 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MISC_FILES/Insomnia_2020-04-30.json: -------------------------------------------------------------------------------- 1 | {"_type":"export","__export_format":4,"__export_date":"2020-04-30T19:06:55.214Z","__export_source":"insomnia.desktop.app:v7.1.1","resources":[{"_id":"req_ed19fb684fd54741929a9c3fa5fdfd1d","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"multipart/form-data","params":[{"fileName":"/home/luizotavio/Imagens/p3.png","id":"pair_cc28bd7f4ab549029bba3ec73cad07c0","name":"foto","type":"file","value":""},{"id":"pair_9bd73cb61b7a4def9f3a058532a92379","name":"aluno_id","value":"6"}]},"created":1573753500582,"description":"","headers":[{"id":"pair_1bfce6b244b44c28be3698d316bf9bf7","name":"Content-Type","value":"multipart/form-data"}],"isPrivate":false,"metaSortKey":-1573753500582,"method":"POST","modified":1576665095997,"name":"Store","parameters":[],"parentId":"fld_a0f5d2acc4744e7e8d927adee74d77c1","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/fotos/","_type":"request"},{"_id":"fld_a0f5d2acc4744e7e8d927adee74d77c1","created":1573753459651,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1573753459652,"modified":1573753459651,"name":"Fotos","parentId":"wrk_df0975ed40074c6c811af23e808ec914","_type":"request_group"},{"_id":"wrk_df0975ed40074c6c811af23e808ec914","created":1573438123460,"description":"","modified":1574088383029,"name":"Escola (Dev)","parentId":null,"_type":"workspace"},{"_id":"req_2ea9552c66c6499bb516268c9fa35983","authentication":{},"body":{},"created":1573668826688,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1573668826689,"method":"GET","modified":1578487292034,"name":"Index","parameters":[],"parentId":"fld_1cc26a262e60483e959a320081626be3","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/alunos/","_type":"request"},{"_id":"fld_1cc26a262e60483e959a320081626be3","created":1573668818253,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1573668818253,"modified":1573668818253,"name":"Aluno","parentId":"wrk_df0975ed40074c6c811af23e808ec914","_type":"request_group"},{"_id":"req_01c37a1b4b324629a78c54a6077aeece","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"nome\": \"João\",\n\t\"sobrenome\": \"Miranda Silva\",\n\t\"email\": \"joão@gmail.com\",\n\t\"idade\": \"37\",\n\t\"peso\": \"110.50\",\n\t\"altura\": \"1.90\"\n}"},"created":1573668863391,"description":"","headers":[{"id":"pair_2dfb5ae488914eb1810693d30bdc93d4","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1573614764566,"method":"POST","modified":1576692692402,"name":"Store","parameters":[],"parentId":"fld_1cc26a262e60483e959a320081626be3","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/alunos/","_type":"request"},{"_id":"req_8429412d16b44aa59c10913e88bde6ed","authentication":{},"body":{},"created":1573668864770,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1573587733504.5,"method":"GET","modified":1588220585732,"name":"Show","parameters":[],"parentId":"fld_1cc26a262e60483e959a320081626be3","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/alunos/18","_type":"request"},{"_id":"req_09f5767d0b1f4a84b3f4e11f822a05f7","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1573668865932,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1573574217973.75,"method":"DELETE","modified":1573836444270,"name":"Delete","parameters":[],"parentId":"fld_1cc26a262e60483e959a320081626be3","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/alunos/13","_type":"request"},{"_id":"req_a8868af867554898a53c866aead74808","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"nome\": \"João\"\n}"},"created":1573668867267,"description":"","headers":[{"id":"pair_90dbfeda02b8473abfb2cf97b4dd55ff","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1573567460208.375,"method":"PUT","modified":1578487338668,"name":"Update","parameters":[],"parentId":"fld_1cc26a262e60483e959a320081626be3","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/alunos/21","_type":"request"},{"_id":"req_45f99ffa46e540c8bdb0ae6316b64c7f","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"email\": \"admin@email.com\",\n\t\"password\": \"123456\"\n}"},"created":1573560702443,"description":"","headers":[{"disabled":false,"id":"pair_e4803e7a436a4df88b37e198ff5f33c6","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1573560702443,"method":"POST","modified":1588271646719,"name":"Store","parameters":[],"parentId":"fld_c9ceec83fb49462cbca485162cee480e","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/tokens/","_type":"request"},{"_id":"fld_c9ceec83fb49462cbca485162cee480e","created":1573560689360,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1573560689360,"modified":1573560689360,"name":"Tokens","parentId":"wrk_df0975ed40074c6c811af23e808ec914","_type":"request_group"},{"_id":"req_e26cc5f8d663481aaa2a58e6a68069f0","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":""},"created":1573527851841,"description":"","headers":[{"id":"pair_9d5276724d1b447dab7ba63d78a1977e","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1573527851841,"method":"DELETE","modified":1573836464154,"name":"Delete","parameters":[],"parentId":"fld_3365a6185f754fbf9d435a509a56ff48","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/users/","_type":"request"},{"_id":"fld_3365a6185f754fbf9d435a509a56ff48","created":1573497641621,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1573497641621,"modified":1573497641621,"name":"Users","parentId":"wrk_df0975ed40074c6c811af23e808ec914","_type":"request_group"},{"_id":"req_9f27c8611512498e913078ecf68e1c66","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"nome\": \"Joana 2\"\n}"},"created":1573527387719,"description":"","headers":[{"id":"pair_079b5e8026e944e4972e06af4ec9413d","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1573527387719,"method":"PUT","modified":1573836469464,"name":"Update","parameters":[],"parentId":"fld_3365a6185f754fbf9d435a509a56ff48","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/users/","_type":"request"},{"_id":"req_4240ca4b75a04698abac00f7db57d54d","authentication":{},"body":{},"created":1573527078764,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1573527078764,"method":"GET","modified":1576805891971,"name":"Show","parameters":[],"parentId":"fld_3365a6185f754fbf9d435a509a56ff48","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/users/1","_type":"request"},{"_id":"req_efd01d5c30664a20936ae9349e96b1a7","authentication":{},"body":{},"created":1573526901364,"description":"","headers":[{"disabled":false,"id":"pair_e133a36cccdb474a85644abffd9a0a4f","name":"authorization","value":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6OCwiZW1haWwiOiJqb2FvRURJVEFET0BlbWFpbC5jb20iLCJpYXQiOjE1NzM1NjEzNzUsImV4cCI6MTU3NDE2NjE3NX0.3PXyD4JhZk7Fq0gvArM5xs4y1idVQleYNbcrK1IfKGI"}],"isPrivate":false,"metaSortKey":-1573526901364,"method":"GET","modified":1573568122917,"name":"Index","parameters":[],"parentId":"fld_3365a6185f754fbf9d435a509a56ff48","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/users/","_type":"request"},{"_id":"req_16358b1dee4744eaaf9ddc2699ced014","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"nome\": \"Luiz Otávio\",\n\t\"password\": \"123456\",\n\t\"email\": \"luiz@gmail.com\"\n}"},"created":1573497650732,"description":"","headers":[{"id":"pair_a05c76aa0f01403e87530edf39234a5d","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1573497650733,"method":"POST","modified":1576801216313,"name":"Store","parameters":[],"parentId":"fld_3365a6185f754fbf9d435a509a56ff48","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/users/","_type":"request"},{"_id":"req_39193a368de1408398ebf6cc62ca3e34","authentication":{},"body":{},"created":1573438255079,"description":"","headers":[{"description":"","disabled":true,"id":"pair_e19e9c555ef64f158d336f72760cb68b","name":"origin","value":"EITA"}],"isPrivate":false,"metaSortKey":-1573438255079,"method":"GET","modified":1588220481584,"name":"Index","parameters":[],"parentId":"fld_c851e4fb2eb944e38cf9c32f0b98923b","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}","_type":"request"},{"_id":"fld_c851e4fb2eb944e38cf9c32f0b98923b","created":1573438209508,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1573438209508,"modified":1573438209508,"name":"Home","parentId":"wrk_df0975ed40074c6c811af23e808ec914","_type":"request_group"},{"_id":"env_edd8b3ee127852f11dd6283791b01a79df7c0fd6","color":null,"created":1573438123678,"data":{"base_url":"http://localhost:3001","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJsdWl6QGdtYWlsLmNvbSIsImlhdCI6MTU4ODIyMDczOSwiZXhwIjoxNTg4ODI1NTM5fQ.e_TtM-p1ypN22H_q7uV0ZYGBzjukBrHh8Mqg0-_tuPk"},"dataPropertyOrder":{"&":["base_url","token"]},"isPrivate":false,"metaSortKey":1573438123678,"modified":1588220793537,"name":"Base Environment","parentId":"wrk_df0975ed40074c6c811af23e808ec914","_type":"environment"},{"_id":"jar_edd8b3ee127852f11dd6283791b01a79df7c0fd6","cookies":[],"created":1573438123680,"modified":1578425234993,"name":"Default Jar","parentId":"wrk_df0975ed40074c6c811af23e808ec914","_type":"cookie_jar"}]} -------------------------------------------------------------------------------- /MISC_FILES/keybindings.json: -------------------------------------------------------------------------------- 1 | // Place your key bindings in this file to override the defaultsauto[] 2 | [ 3 | { 4 | "key": "ctrl+shift+alt+=", 5 | "command": "editor.action.fontZoomIn" 6 | }, 7 | { 8 | "key": "ctrl+shift+alt+-", 9 | "command": "editor.action.fontZoomOut" 10 | }, 11 | { 12 | "key": "ctrl+shift+alt+0", 13 | "command": "editor.action.fontZoomReset" 14 | }, 15 | { 16 | "key": "ctrl+alt+d", 17 | "command": "editor.action.copyLinesDownAction", 18 | "when": "editorTextFocus && !editorReadonly" 19 | }, 20 | { 21 | "key": "ctrl+shift+alt+down", 22 | "command": "-editor.action.copyLinesDownAction", 23 | "when": "editorTextFocus && !editorReadonly" 24 | }, 25 | { 26 | "key": "ctrl+alt+s", 27 | "command": "editor.action.copyLinesUpAction", 28 | "when": "editorTextFocus && !editorReadonly" 29 | }, 30 | { 31 | "key": "ctrl+shift+alt+up", 32 | "command": "-editor.action.copyLinesUpAction", 33 | "when": "editorTextFocus && !editorReadonly" 34 | }, 35 | { 36 | "key": "ctrl+numpad_divide", 37 | "command": "editor.action.commentLine", 38 | "when": "editorTextFocus && !editorReadonly" 39 | }, 40 | { 41 | "key": "ctrl+/", 42 | "command": "-editor.action.commentLine", 43 | "when": "editorTextFocus && !editorReadonly" 44 | }, 45 | { 46 | "key": "ctrl+shift+alt+k", 47 | "command": "code-runner.runCustomCommand" 48 | }, 49 | { 50 | "key": "ctrl+alt+k", 51 | "command": "-code-runner.runCustomCommand" 52 | } 53 | ] -------------------------------------------------------------------------------- /MISC_FILES/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // VS Code 3 | "workbench.startupEditor": "newUntitledFile", 4 | "workbench.editor.labelFormat": "short", 5 | "workbench.colorTheme": "Dracula", 6 | "workbench.iconTheme": "material-icon-theme", 7 | "workbench.editor.enablePreview": false, 8 | 9 | "editor.fontFamily": "'Liga Droid Sans Mono'", 10 | "editor.tabSize": 2, 11 | "editor.formatOnSave": false, 12 | "editor.fontLigatures": false, 13 | "editor.renderLineHighlight": "gutter", 14 | "editor.rulers": [ 15 | 79, 16 | 120, 17 | ], 18 | "editor.minimap.enabled": false, 19 | "editor.fontSize": 18, 20 | 21 | "debug.console.fontSize": 18, 22 | "toggleFileEventLogging": true, 23 | "explorer.compactFolders": false, 24 | "breadcrumbs.enabled": true, 25 | "git.enableSmartCommit": true, 26 | "window.zoomLevel": 0, 27 | "diffEditor.ignoreTrimWhitespace": false, 28 | "material-icon-theme.hidesExplorerArrows": true, 29 | 30 | // Auto fix on save 31 | "editor.codeActionsOnSave": { 32 | "source.fixAll.eslint": true, 33 | "source.fixAll": true 34 | }, 35 | 36 | // Terminal 37 | "terminal.integrated.copyOnSelection": true, 38 | "terminal.integrated.cursorBlinking": true, 39 | "terminal.integrated.cursorStyle": "line", 40 | "terminal.integrated.shell.linux": "zsh", 41 | "terminal.integrated.fontFamily": "'Fira Code'", 42 | "terminal.integrated.fontSize": 18, 43 | 44 | // Javascript & TypeScript 45 | "javascript.preferences.quoteStyle": "single", 46 | "typescript.preferences.quoteStyle": "single", 47 | "javascript.validate.enable": false, 48 | "javascript.updateImportsOnFileMove.enabled": "never", 49 | "typescript.updateImportsOnFileMove.enabled": "never", 50 | 51 | "emmet.syntaxProfiles": { 52 | "javascript": "jsx" 53 | }, 54 | 55 | "emmet.includeLanguages": { 56 | "django-html": "html", 57 | "javascript": "javascriptreact" 58 | }, 59 | "[javascript]": { 60 | "editor.defaultFormatter": "vscode.typescript-language-features" 61 | }, 62 | 63 | // Python 64 | "python.linting.pylintArgs": [ 65 | "--load-plugins=pylint_django", 66 | "--errors-only", 67 | ], 68 | "[python]": { 69 | "editor.tabSize": 4, 70 | "editor.insertSpaces": true, 71 | "editor.formatOnSave": true, 72 | "editor.formatOnType": true 73 | }, 74 | "python.pythonPath": "/bin/python3.8", 75 | "python.linting.flake8Enabled": true, 76 | "python.linting.mypyEnabled": true, 77 | "python.testing.unittestEnabled": true, 78 | 79 | // Django 80 | "files.associations": { 81 | "*.html": "html", 82 | "**/templates/*.html": "django-html", 83 | "**/templates/*": "django-txt", 84 | "**/requirements{/**,*}.{txt,in}": "pip-requirements" 85 | }, 86 | 87 | // AutoDocstring extension 88 | "autoDocstring.docstringFormat": "google", 89 | 90 | // BracketPairColorizer extension 91 | "bracketPairColorizer.showBracketsInGutter": true, 92 | 93 | // Code runner extension 94 | "code-runner.clearPreviousOutput": true, 95 | "code-runner.ignoreSelection": true, 96 | "code-runner.saveFileBeforeRun": true, 97 | "code-runner.runInTerminal": true, 98 | "code-runner.executorMap": { 99 | "python": "python3.8 -u" 100 | }, 101 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uma API REST simples para consumo de dados 2 | 3 | Atenção: você precisa ter o NodeJS instalado no seu computador. 4 | 5 | Para subir o projeto no ar com SQLite, copie o arquivo `.env_example` para `.env`. 6 | 7 | Você também precisará adicionar uma secret key no arquivo `.env`: 8 | 9 | ``` 10 | TOKEN_SECRET='sua_secret_key_aqui' 11 | ``` 12 | 13 | Execute os comandos abaixo: 14 | 15 | ``` 16 | npm i 17 | npx sequelize db:migrate 18 | npx sequelize db:seed:all 19 | npm run dev 20 | ``` 21 | 22 | Neste ponto sua API deverá está rodando no endereço http://127.0.0.1:3001/. 23 | 24 | Caso queira migrar para MySQL/MariaDB, edite as configurações de base de dados no arquivo `.env`, configure também o `src/config/database.js`. 25 | 26 | Para SQLite as configurações são: 27 | 28 | ```javascript 29 | require('dotenv').config(); 30 | 31 | module.exports = { 32 | dialect: 'sqlite', 33 | storage: './db.sqlite', 34 | define: { 35 | timestamps: true, 36 | underscored: true, 37 | underscoredAll: true, 38 | createdAt: 'created_at', 39 | updatedAt: 'updated_at', 40 | }, 41 | }; 42 | ``` 43 | 44 | Para MySQL/MariaDB as configurações são: 45 | 46 | ```javascript 47 | require('dotenv').config(); 48 | 49 | module.exports = { 50 | host: process.env.DATABASE_HOST, 51 | port: process.env.DATABASE_PORT, 52 | username: process.env.DATABASE_USERNAME, 53 | password: process.env.DATABASE_PASSWORD, 54 | database: process.env.DATABASE, 55 | dialectOptions: { 56 | timezone: 'America/Sao_Paulo', 57 | }, 58 | timezone: 'America/Sao_Paulo', 59 | 60 | define: { 61 | timestamps: true, 62 | underscored: true, 63 | underscoredAll: true, 64 | createdAt: 'created_at', 65 | updatedAt: 'updated_at', 66 | }, 67 | }; 68 | ``` 69 | 70 | Perceba que as configurações começando com `process.env.` vem do arquivo `.env`. 71 | 72 | Os dados de usuário e senha dos arquivos de seed são: 73 | 74 | - email = admin@email.com 75 | - senha = 123456 76 | 77 | Você pode obter o token JWT na rota `/tokens`, passando os dados JSON: 78 | 79 | ```json 80 | { 81 | "email": "admin@email.com", 82 | "password": "123456" 83 | } 84 | ``` 85 | 86 | Headers: 87 | 88 | ``` 89 | Content-Type application/json; charset=utf-8 90 | ``` 91 | # endpoints 92 | 93 | Os seguintes endpoints estão configurados: 94 | 95 | ## Home - não há nada aqui 96 | 97 | - `/` - GET 98 | 99 | ## Usuários (users) 100 | 101 | - `/users` - DELETE - Apaga o usuário logado 102 | - `/users` - PUT - Atualiza o usuário logado 103 | - `/users` - POST - Cria um usuário 104 | - `/users/:id` - GET - Mostra o usuário do ID enviado (rota desativada) 105 | - `/users` - GET - Mostra todos os usuários (rota desativada) 106 | 107 | **Dados para usuários (JSON)** 108 | 109 | ``` 110 | { 111 | "nome": "nome válido", 112 | "password": "senha válida", 113 | "email": "email_valido@email.com" 114 | } 115 | ``` 116 | 117 | ## Tokens 118 | 119 | - `/tokens` - POST - Obtém o token JWT 120 | 121 | **Dados para tokens (JSON)** 122 | 123 | ``` 124 | { 125 | "email": "admin@email.com", 126 | "password": "123456" 127 | } 128 | ``` 129 | 130 | ## Aluno 131 | 132 | - `/alunos/:id` - DELETE - Apaga o aluno do ID enviado 133 | - `/alunos/:id` - PUT - Atualiza o aluno do ID enviado 134 | - `/alunos` - POST - Cria um aluno 135 | - `/alunos/:id` - GET - Mostra o aluno do ID enviado 136 | - `/alunos` - GET - Mostra todos os alunos 137 | 138 | 139 | **Dados para tokens (JSON)** 140 | 141 | ``` 142 | { 143 | "nome": "Nome", 144 | "sobrenome": "Sobrenome", 145 | "email": "email@email.com", 146 | "idade": "50", 147 | "peso": "80.04", 148 | "altura": "1.90" 149 | } 150 | ``` 151 | 152 | ## Fotos 153 | 154 | Atenção aqui, esse é o único endpoint `multipart/form-data` para envio de arquivos. 155 | 156 | - `/fotos` - POST - Recebe um arquivo de foto JPG ou PNG e um `aluno_id`. 157 | 158 | **Dados para fotos (multipart/form-data)** 159 | 160 | ``` 161 | { 162 | "foto": (ARQUIVO.PNG|JPG), 163 | "aluno_id": ":id" 164 | } 165 | ``` -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "execMap": { 3 | "js": "node -r sucrase/register" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api_rest", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "dev": "nodemon src/server.js", 8 | "build": "sucrase ./src -d ./dist --transforms imports", 9 | "start": "node dist/server.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "eslint": "^7.9.0", 16 | "eslint-config-airbnb-base": "^14.2.0", 17 | "eslint-plugin-import": "^2.22.0", 18 | "nodemon": "^2.0.4", 19 | "sequelize-cli": "^6.2.0", 20 | "sucrase": "^3.15.0" 21 | }, 22 | "dependencies": { 23 | "bcryptjs": "^2.4.3", 24 | "cors": "^2.8.5", 25 | "dotenv": "^8.2.0", 26 | "express": "^4.17.1", 27 | "helmet": "^4.1.1", 28 | "jsonwebtoken": "^8.5.1", 29 | "mariadb": "^2.4.2", 30 | "multer": "^1.4.2", 31 | "sequelize": "^6.3.5", 32 | "sqlite3": "^5.0.0" 33 | }, 34 | "description": "" 35 | } 36 | -------------------------------------------------------------------------------- /setup-linux-mac.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cp .env_example .env 3 | npm i 4 | npx sequelize db:migrate 5 | npx sequelize db:seed:all 6 | npm run dev -------------------------------------------------------------------------------- /setup-windows.bat: -------------------------------------------------------------------------------- 1 | copy .env_example .env 2 | call npm i 3 | call npx sequelize db:migrate 4 | call npx sequelize db:seed:all 5 | call npm run dev 6 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | 3 | import './database'; 4 | 5 | import express from 'express'; 6 | import cors from 'cors'; 7 | import helmet from 'helmet'; 8 | 9 | import homeRoutes from './routes/homeRoutes'; 10 | import userRoutes from './routes/userRoutes'; 11 | import tokenRoutes from './routes/tokenRoutes'; 12 | import alunoRoutes from './routes/alunoRoutes'; 13 | import fotoRoutes from './routes/fotoRoutes'; 14 | 15 | const whiteList = [ 16 | 'https://react1.otaviomiranda.com.br', 17 | 'https://react2.otaviomiranda.com.br', 18 | 'http://localhost:3000', 19 | ]; 20 | 21 | const corsOptions = { 22 | origin: function (origin, callback) { 23 | if(whiteList.indexOf(origin) !== -1 || !origin) { 24 | callback(null, true); 25 | } else { 26 | callback(new Error('Not allowed by CORS')); 27 | } 28 | } 29 | }; 30 | 31 | class App { 32 | constructor() { 33 | this.app = express(); 34 | this.middlewares(); 35 | this.routes(); 36 | } 37 | 38 | middlewares() { 39 | this.app.use(cors(corsOptions)); 40 | this.app.use(helmet()); 41 | this.app.use(express.urlencoded({ extended: true })); 42 | this.app.use(express.json()); 43 | this.app.use('/images/', express.static(resolve(__dirname, '..', 'uploads', 'images'))); 44 | } 45 | 46 | routes() { 47 | this.app.use('/', homeRoutes); 48 | this.app.use('/users/', userRoutes); 49 | this.app.use('/tokens/', tokenRoutes); 50 | this.app.use('/alunos/', alunoRoutes); 51 | this.app.use('/fotos/', fotoRoutes); 52 | } 53 | } 54 | 55 | export default new App().app; 56 | -------------------------------------------------------------------------------- /src/config/appConfig.js: -------------------------------------------------------------------------------- 1 | export default { 2 | url: process.env.APP_URL, 3 | }; 4 | -------------------------------------------------------------------------------- /src/config/database.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = { 4 | /* SQLite */ 5 | dialect: 'sqlite', 6 | storage: './db.sqlite', 7 | 8 | /* MySQL / MariaDB */ 9 | // host: process.env.DATABASE_HOST, 10 | // port: process.env.DATABASE_PORT, 11 | // username: process.env.DATABASE_USERNAME, 12 | // password: process.env.DATABASE_PASSWORD, 13 | // database: process.env.DATABASE, 14 | // dialectOptions: { 15 | // timezone: 'America/Sao_Paulo', 16 | // }, 17 | // timezone: 'America/Sao_Paulo', 18 | 19 | /* ALL */ 20 | define: { 21 | timestamps: true, 22 | underscored: true, 23 | underscoredAll: true, 24 | createdAt: 'created_at', 25 | updatedAt: 'updated_at', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/config/multerConfig.js: -------------------------------------------------------------------------------- 1 | import multer from 'multer'; 2 | import { extname, resolve } from 'path'; 3 | 4 | const aleatorio = () => Math.floor(Math.random() * 10000 + 10000); 5 | 6 | export default { 7 | fileFilter: (req, file, cb) => { 8 | if (file.mimetype !== 'image/png' && file.mimetype !== 'image/jpeg') { 9 | return cb(new multer.MulterError('Arquivo precisa ser PNG ou JPG.')); 10 | } 11 | 12 | return cb(null, true); 13 | }, 14 | storage: multer.diskStorage({ 15 | destination: (req, file, cb) => { 16 | cb(null, resolve(__dirname, '..', '..', 'uploads', 'images')); 17 | }, 18 | filename: (req, file, cb) => { 19 | cb(null, `${Date.now()}_${aleatorio()}${extname(file.originalname)}`); 20 | }, 21 | }), 22 | }; 23 | -------------------------------------------------------------------------------- /src/controllers/AlunoController.js: -------------------------------------------------------------------------------- 1 | import Aluno from '../models/Aluno'; 2 | import Foto from '../models/Foto'; 3 | 4 | class AlunoController { 5 | async index(req, res) { 6 | const alunos = await Aluno.findAll({ 7 | attributes: ['id', 'nome', 'sobrenome', 'email', 'idade', 'peso', 'altura'], 8 | order: [['id', 'DESC'], [Foto, 'id', 'DESC']], 9 | include: { 10 | model: Foto, 11 | attributes: ['url', 'filename'], 12 | }, 13 | }); 14 | res.json(alunos); 15 | } 16 | 17 | async store(req, res) { 18 | try { 19 | const aluno = await Aluno.create(req.body); 20 | 21 | return res.json(aluno); 22 | } catch (e) { 23 | return res.status(400).json({ 24 | errors: e.errors.map((err) => err.message), 25 | }); 26 | } 27 | } 28 | 29 | async show(req, res) { 30 | try { 31 | const { id } = req.params; 32 | 33 | if (!id) { 34 | return res.status(400).json({ 35 | errors: ['Faltando ID'], 36 | }); 37 | } 38 | 39 | const aluno = await Aluno.findByPk(id, { 40 | attributes: ['id', 'nome', 'sobrenome', 'email', 'idade', 'peso', 'altura'], 41 | order: [['id', 'DESC'], [Foto, 'id', 'DESC']], 42 | include: { 43 | model: Foto, 44 | attributes: ['url', 'filename'], 45 | }, 46 | }); 47 | 48 | if (!aluno) { 49 | return res.status(400).json({ 50 | errors: ['Aluno não existe'], 51 | }); 52 | } 53 | 54 | return res.json(aluno); 55 | } catch (e) { 56 | return res.status(400).json({ 57 | errors: e.errors.map((err) => err.message), 58 | }); 59 | } 60 | } 61 | 62 | async delete(req, res) { 63 | try { 64 | const { id } = req.params; 65 | 66 | if (!id) { 67 | return res.status(400).json({ 68 | errors: ['Faltando ID'], 69 | }); 70 | } 71 | 72 | const aluno = await Aluno.findByPk(id); 73 | 74 | if (!aluno) { 75 | return res.status(400).json({ 76 | errors: ['Aluno não existe'], 77 | }); 78 | } 79 | 80 | await aluno.destroy(); 81 | return res.json({ 82 | apagado: true, 83 | }); 84 | } catch (e) { 85 | return res.status(400).json({ 86 | errors: e.errors.map((err) => err.message), 87 | }); 88 | } 89 | } 90 | 91 | async update(req, res) { 92 | try { 93 | const { id } = req.params; 94 | 95 | if (!id) { 96 | return res.status(400).json({ 97 | errors: ['Faltando ID'], 98 | }); 99 | } 100 | 101 | const aluno = await Aluno.findByPk(id); 102 | 103 | if (!aluno) { 104 | return res.status(400).json({ 105 | errors: ['Aluno não existe'], 106 | }); 107 | } 108 | 109 | const alunoAtualizado = await aluno.update(req.body); 110 | return res.json(alunoAtualizado); 111 | } catch (e) { 112 | return res.status(400).json({ 113 | errors: e.errors.map((err) => err.message), 114 | }); 115 | } 116 | } 117 | } 118 | 119 | export default new AlunoController(); 120 | -------------------------------------------------------------------------------- /src/controllers/FotoController.js: -------------------------------------------------------------------------------- 1 | import multer from 'multer'; 2 | import multerConfig from '../config/multerConfig'; 3 | 4 | import Foto from '../models/Foto'; 5 | 6 | const upload = multer(multerConfig).single('foto'); 7 | 8 | class FotoController { 9 | store(req, res) { 10 | return upload(req, res, async (error) => { 11 | if (error) { 12 | return res.status(400).json({ 13 | errors: [error.code], 14 | }); 15 | } 16 | 17 | try { 18 | const { originalname, filename } = req.file; 19 | const { aluno_id } = req.body; 20 | const foto = await Foto.create({ originalname, filename, aluno_id }); 21 | 22 | return res.json(foto); 23 | } catch (e) { 24 | return res.status(400).json({ 25 | errors: ['Aluno não existe'], 26 | }); 27 | } 28 | }); 29 | } 30 | } 31 | 32 | export default new FotoController(); 33 | -------------------------------------------------------------------------------- /src/controllers/HomeController.js: -------------------------------------------------------------------------------- 1 | class HomeController { 2 | async index(req, res) { 3 | res.json('Index'); 4 | } 5 | } 6 | 7 | export default new HomeController(); 8 | -------------------------------------------------------------------------------- /src/controllers/TokenController.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import User from '../models/User'; 3 | 4 | class TokenController { 5 | async store(req, res) { 6 | const { email = '', password = '' } = req.body; 7 | 8 | if (!email || !password) { 9 | return res.status(401).json({ 10 | errors: ['Credenciais inválidas'], 11 | }); 12 | } 13 | 14 | const user = await User.findOne({ where: { email } }); 15 | 16 | if (!user) { 17 | return res.status(401).json({ 18 | errors: ['Usuário não existe'], 19 | }); 20 | } 21 | 22 | if (!(await user.passwordIsValid(password))) { 23 | return res.status(401).json({ 24 | errors: ['Senha inválida'], 25 | }); 26 | } 27 | 28 | const { id } = user; 29 | const token = jwt.sign({ id, email }, process.env.TOKEN_SECRET, { 30 | expiresIn: process.env.TOKEN_EXPIRATION, 31 | }); 32 | 33 | return res.json({ token, user: { nome: user.nome, id, email } }); 34 | } 35 | } 36 | 37 | export default new TokenController(); 38 | -------------------------------------------------------------------------------- /src/controllers/UserController.js: -------------------------------------------------------------------------------- 1 | import User from '../models/User'; 2 | 3 | class UserController { 4 | async store(req, res) { 5 | try { 6 | const novoUser = await User.create(req.body); 7 | const { id, nome, email } = novoUser; 8 | return res.json({ id, nome, email }); 9 | } catch (e) { 10 | return res.status(400).json({ 11 | errors: e.errors.map((err) => err.message), 12 | }); 13 | } 14 | } 15 | 16 | // Index 17 | async index(req, res) { 18 | try { 19 | const users = await User.findAll({ attributes: ['id', 'nome', 'email'] }); 20 | return res.json(users); 21 | } catch (e) { 22 | return res.json(null); 23 | } 24 | } 25 | 26 | // Show 27 | async show(req, res) { 28 | try { 29 | const user = await User.findByPk(req.params.id); 30 | 31 | const { id, nome, email } = user; 32 | return res.json({ id, nome, email }); 33 | } catch (e) { 34 | return res.json(null); 35 | } 36 | } 37 | 38 | // Update 39 | async update(req, res) { 40 | try { 41 | const user = await User.findByPk(req.userId); 42 | 43 | if (!user) { 44 | return res.status(400).json({ 45 | errors: ['Usuário não existe'], 46 | }); 47 | } 48 | 49 | const novosDados = await user.update(req.body); 50 | const { id, nome, email } = novosDados; 51 | return res.json({ id, nome, email }); 52 | } catch (e) { 53 | return res.status(400).json({ 54 | errors: e.errors.map((err) => err.message), 55 | }); 56 | } 57 | } 58 | 59 | // Delete 60 | async delete(req, res) { 61 | try { 62 | const user = await User.findByPk(req.userId); 63 | 64 | if (!user) { 65 | return res.status(400).json({ 66 | errors: ['Usuário não existe'], 67 | }); 68 | } 69 | 70 | await user.destroy(); 71 | return res.json(null); 72 | } catch (e) { 73 | return res.status(400).json({ 74 | errors: e.errors.map((err) => err.message), 75 | }); 76 | } 77 | } 78 | } 79 | 80 | export default new UserController(); 81 | -------------------------------------------------------------------------------- /src/database/index.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import databaseConfig from '../config/database'; 3 | import Aluno from '../models/Aluno'; 4 | import User from '../models/User'; 5 | import Foto from '../models/Foto'; 6 | 7 | const models = [Aluno, User, Foto]; 8 | 9 | const connection = new Sequelize(databaseConfig); 10 | 11 | models.forEach((model) => model.init(connection)); 12 | models.forEach((model) => model.associate && model.associate(connection.models)); 13 | -------------------------------------------------------------------------------- /src/database/migrations/20191111123839-alunos.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable('alunos', { 3 | id: { 4 | type: Sequelize.INTEGER, 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | }, 9 | nome: { 10 | type: Sequelize.STRING, 11 | allowNull: false, 12 | }, 13 | sobrenome: { 14 | type: Sequelize.STRING, 15 | allowNull: false, 16 | }, 17 | email: { 18 | type: Sequelize.STRING, 19 | allowNull: false, 20 | }, 21 | idade: { 22 | type: Sequelize.INTEGER, 23 | allowNull: false, 24 | }, 25 | peso: { 26 | type: Sequelize.FLOAT, 27 | allowNull: false, 28 | }, 29 | altura: { 30 | type: Sequelize.FLOAT, 31 | allowNull: false, 32 | }, 33 | created_at: { 34 | type: Sequelize.DATE, 35 | allowNull: false, 36 | }, 37 | updated_at: { 38 | type: Sequelize.DATE, 39 | allowNull: false, 40 | }, 41 | }), 42 | 43 | down: (queryInterface) => queryInterface.dropTable('alunos'), 44 | }; 45 | -------------------------------------------------------------------------------- /src/database/migrations/20191111181936-users.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable('users', { 3 | id: { 4 | type: Sequelize.INTEGER, 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | }, 9 | nome: { 10 | type: Sequelize.STRING, 11 | allowNull: false, 12 | }, 13 | email: { 14 | type: Sequelize.STRING, 15 | allowNull: false, 16 | unique: true, 17 | }, 18 | password_hash: { 19 | type: Sequelize.STRING, 20 | allowNull: false, 21 | }, 22 | created_at: { 23 | type: Sequelize.DATE, 24 | allowNull: false, 25 | }, 26 | updated_at: { 27 | type: Sequelize.DATE, 28 | allowNull: false, 29 | }, 30 | }), 31 | 32 | down: (queryInterface) => queryInterface.dropTable('users'), 33 | }; 34 | -------------------------------------------------------------------------------- /src/database/migrations/20191113141734-mudar-email-aluno-unique.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.changeColumn( 3 | 'alunos', 4 | 'email', 5 | { 6 | type: Sequelize.STRING, 7 | allowNull: false, 8 | unique: true, 9 | }, 10 | ), 11 | 12 | down: () => {}, 13 | }; 14 | -------------------------------------------------------------------------------- /src/database/migrations/20191115165102-criar-tabela-de-foto-do-aluno.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable('fotos', { 3 | id: { 4 | type: Sequelize.INTEGER, 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | }, 9 | originalname: { 10 | type: Sequelize.STRING, 11 | allowNull: false, 12 | }, 13 | filename: { 14 | type: Sequelize.STRING, 15 | allowNull: false, 16 | }, 17 | aluno_id: { 18 | type: Sequelize.INTEGER, 19 | allowNull: true, 20 | references: { 21 | model: 'alunos', 22 | key: 'id', 23 | }, 24 | onDelete: 'SET NULL', 25 | onUpdate: 'CASCADE', 26 | }, 27 | created_at: { 28 | type: Sequelize.DATE, 29 | allowNull: false, 30 | }, 31 | updated_at: { 32 | type: Sequelize.DATE, 33 | allowNull: false, 34 | }, 35 | }), 36 | 37 | down: (queryInterface) => queryInterface.dropTable('fotos'), 38 | }; 39 | -------------------------------------------------------------------------------- /src/database/seeds/20191113015433-criar-usuarios.js: -------------------------------------------------------------------------------- 1 | const bcryptjs = require('bcryptjs'); 2 | 3 | module.exports = { 4 | up: async (queryInterface) => queryInterface.bulkInsert( 5 | 'users', 6 | [ 7 | { 8 | nome: 'admin', 9 | email: 'admin@email.com', 10 | password_hash: await bcryptjs.hash('123456', 8), 11 | created_at: new Date(), 12 | updated_at: new Date(), 13 | }, 14 | ], 15 | {}, 16 | ), 17 | 18 | down: () => {}, 19 | }; 20 | -------------------------------------------------------------------------------- /src/middlewares/loginRequired.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import User from '../models/User'; 3 | 4 | export default async (req, res, next) => { 5 | const { authorization } = req.headers; 6 | 7 | if (!authorization) { 8 | return res.status(401).json({ 9 | errors: ['Login required'], 10 | }); 11 | } 12 | 13 | const [, token] = authorization.split(' '); 14 | 15 | try { 16 | const dados = jwt.verify(token, process.env.TOKEN_SECRET); 17 | const { id, email } = dados; 18 | 19 | const user = await User.findOne({ 20 | where: { 21 | id, 22 | email, 23 | }, 24 | }); 25 | 26 | if (!user) { 27 | return res.status(401).json({ 28 | errors: ['Usuário inválido'], 29 | }); 30 | } 31 | 32 | req.userId = id; 33 | req.userEmail = email; 34 | return next(); 35 | } catch (e) { 36 | return res.status(401).json({ 37 | errors: ['Token expirado ou inválido.'], 38 | }); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/models/Aluno.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from 'sequelize'; 2 | 3 | export default class Aluno extends Model { 4 | static init(sequelize) { 5 | super.init({ 6 | nome: { 7 | type: Sequelize.STRING, 8 | defaultValue: '', 9 | validate: { 10 | len: { 11 | args: [3, 255], 12 | msg: 'Nome precisa ter entre 3 e 255 caracteres.', 13 | }, 14 | }, 15 | }, 16 | sobrenome: { 17 | type: Sequelize.STRING, 18 | defaultValue: '', 19 | validate: { 20 | len: { 21 | args: [3, 255], 22 | msg: 'Sobrenome precisa ter entre 3 e 255 caracteres.', 23 | }, 24 | }, 25 | }, 26 | email: { 27 | type: Sequelize.STRING, 28 | defaultValue: '', 29 | unique: { 30 | msg: 'E-mail já existe', 31 | }, 32 | validate: { 33 | isEmail: { 34 | msg: 'E-mail inválido', 35 | }, 36 | }, 37 | }, 38 | idade: { 39 | type: Sequelize.INTEGER, 40 | defaultValue: '', 41 | validate: { 42 | isInt: { 43 | msg: 'Idade precisa ser um número inteiro', 44 | }, 45 | }, 46 | }, 47 | peso: { 48 | type: Sequelize.FLOAT, 49 | defaultValue: '', 50 | validate: { 51 | isFloat: { 52 | msg: 'Peso precisa ser um número inteiro ou de ponto flutuante', 53 | }, 54 | }, 55 | }, 56 | altura: { 57 | type: Sequelize.FLOAT, 58 | defaultValue: '', 59 | validate: { 60 | isFloat: { 61 | msg: 'Altura precisa ser um número inteiro ou de ponto flutuante', 62 | }, 63 | }, 64 | }, 65 | }, { 66 | sequelize, 67 | }); 68 | return this; 69 | } 70 | 71 | static associate(models) { 72 | this.hasMany(models.Foto, { foreignKey: 'aluno_id' }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/models/Foto.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from 'sequelize'; 2 | import appConfig from '../config/appConfig'; 3 | 4 | export default class Foto extends Model { 5 | static init(sequelize) { 6 | super.init({ 7 | originalname: { 8 | type: Sequelize.STRING, 9 | defaultValue: '', 10 | validate: { 11 | notEmpty: { 12 | msg: 'Campo não pode ficar vazio.', 13 | }, 14 | }, 15 | }, 16 | filename: { 17 | type: Sequelize.STRING, 18 | defaultValue: '', 19 | validate: { 20 | notEmpty: { 21 | msg: 'Campo não pode ficar vazio.', 22 | }, 23 | }, 24 | }, 25 | url: { 26 | type: Sequelize.VIRTUAL, 27 | get() { 28 | return `${appConfig.url}/images/${this.getDataValue('filename')}`; 29 | }, 30 | }, 31 | }, { 32 | sequelize, 33 | tableName: 'fotos', 34 | }); 35 | return this; 36 | } 37 | 38 | static associate(models) { 39 | this.belongsTo(models.Aluno, { foreignKey: 'aluno_id' }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/models/User.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from 'sequelize'; 2 | import bcryptjs from 'bcryptjs'; 3 | 4 | export default class User extends Model { 5 | static init(sequelize) { 6 | super.init({ 7 | nome: { 8 | type: Sequelize.STRING, 9 | defaultValue: '', 10 | validate: { 11 | len: { 12 | args: [3, 255], 13 | msg: 'Campo nome deve ter entre 3 e 255 caracteres', 14 | }, 15 | }, 16 | }, 17 | email: { 18 | type: Sequelize.STRING, 19 | defaultValue: '', 20 | unique: { 21 | msg: 'Email já existe', 22 | }, 23 | validate: { 24 | isEmail: { 25 | msg: 'Email inválido', 26 | }, 27 | }, 28 | }, 29 | password_hash: { 30 | type: Sequelize.STRING, 31 | defaultValue: '', 32 | }, 33 | password: { 34 | type: Sequelize.VIRTUAL, 35 | defaultValue: '', 36 | validate: { 37 | len: { 38 | args: [6, 50], 39 | msg: 'A senha precisa ter entre 6 e 50 caracteres', 40 | }, 41 | }, 42 | }, 43 | }, { 44 | sequelize, 45 | }); 46 | 47 | this.addHook('beforeSave', async (user) => { 48 | if (user.password) { 49 | user.password_hash = await bcryptjs.hash(user.password, 8); 50 | } 51 | }); 52 | 53 | return this; 54 | } 55 | 56 | passwordIsValid(password) { 57 | return bcryptjs.compare(password, this.password_hash); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/routes/alunoRoutes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import alunoController from '../controllers/AlunoController'; 3 | 4 | import loginRequired from '../middlewares/loginRequired'; 5 | 6 | const router = new Router(); 7 | 8 | router.get('/', alunoController.index); 9 | router.post('/', loginRequired, alunoController.store); 10 | router.put('/:id', loginRequired, alunoController.update); 11 | router.get('/:id', alunoController.show); 12 | router.delete('/:id', loginRequired, alunoController.delete); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /src/routes/fotoRoutes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import loginRequired from '../middlewares/loginRequired'; 3 | 4 | import fotoController from '../controllers/FotoController'; 5 | 6 | const router = new Router(); 7 | 8 | router.post('/', loginRequired, fotoController.store); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /src/routes/homeRoutes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import homeController from '../controllers/HomeController'; 3 | 4 | const router = new Router(); 5 | 6 | router.get('/', homeController.index); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /src/routes/tokenRoutes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import tokenController from '../controllers/TokenController'; 3 | 4 | const router = new Router(); 5 | 6 | router.post('/', tokenController.store); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /src/routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import userController from '../controllers/UserController'; 3 | 4 | import loginRequired from '../middlewares/loginRequired'; 5 | 6 | const router = new Router(); 7 | 8 | // Não deveria existir 9 | // router.get('/', userController.index); // Lista usuários 10 | // router.get('/:id', userController.show); // Lista usuário 11 | 12 | router.post('/', userController.store); 13 | router.put('/', loginRequired, userController.update); 14 | router.delete('/', loginRequired, userController.delete); 15 | 16 | export default router; 17 | 18 | /* 19 | index -> lista todos os usuários -> GET 20 | store/create -> cria um novo usuário -> POST 21 | delete -> apaga um usuário -> DELETE 22 | show -> mostra um usuário -> GET 23 | update -> atualiza um usuário -> PATCH ou PUT 24 | */ 25 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | 3 | const port = process.env.APP_PORT; 4 | app.listen(port); 5 | -------------------------------------------------------------------------------- /uploads/images/images_here.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizomf/simple_api_rest_sqlite/cd5b09d2b06486e7c1df595a50f9fa1fb69a534a/uploads/images/images_here.txt --------------------------------------------------------------------------------