From 4a2f25eb93b31265fdaa3e635e5d285a100fedbc Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Fri, 11 Nov 2016 19:50:10 -0200 Subject: [PATCH 01/25] Editando o README para o backend --- README.md | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 020c3f3..501720f 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,8 @@ -# Comunidade NodeBR +# Backend NodeBR -Este repositório engloba o frontend e o backend do site oficial da comunidade +Este repositório refere-se ao backend da comunidade NodeBR hospedada no website https://nodebr.org. -Todos estão convidados a participar, basta escolher em qual parte do projeto -você deseja contribuir. - -### Backend - -O nosso backend é uma aplicação simples rodando Node.js e conectada a um banco -de dados. Pessoas sem muita experiência não terão problemas em entender o código -e pode *botar a mão na massa* em pouco tempo. - -Envie PR para a branch: [backend](https://github.com/nodebr/nodebr/tree/backend) - -### Frontend - -Nosso frontend é um pouco mais complicado pois os interessados terão que entender -um pouco de React, Redux e WebPack. Caso você seja familiar com estas tecnologias, -mesmo que ha pouco tempo, está convidado a enviar sua PR. - -Envie PR para a branch: [frontend](https://github.com/nodebr/nodebr/tree/frontend) - ### Canais de ajuda Antes de iniciar sua contribuição é recomendável que você acesse nosso canal no From 607ebadf97bfc6dfdaba2f855aac23e874aa30a6 Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Mon, 14 Nov 2016 23:33:05 -0200 Subject: [PATCH 02/25] Commit inicial --- .editorconfig | 16 ++ .env | 3 + .gitignore | 271 ++++++++++++++++++ index.js | 10 + lib/db/base.js | 11 + lib/db/index.js | 25 ++ lib/db/knexfile.js | 7 + .../migrations/20161114212251_hello_world.js | 24 ++ lib/db/truncate.js | 25 ++ lib/error-handler.js | 29 ++ lib/server.js | 29 ++ lib/validator.js | 41 +++ package.json | 43 +++ resources/hello-world/handlers.js | 25 ++ resources/hello-world/model.js | 9 + resources/hello-world/routes.js | 25 ++ resources/hello-world/schemas.js | 17 ++ test/fixtures/hello-world.js | 28 ++ test/fixtures/index.js | 15 + test/resources/hello-world.js | 106 +++++++ 20 files changed, 759 insertions(+) create mode 100644 .editorconfig create mode 100644 .env create mode 100644 .gitignore create mode 100644 index.js create mode 100644 lib/db/base.js create mode 100644 lib/db/index.js create mode 100644 lib/db/knexfile.js create mode 100644 lib/db/migrations/20161114212251_hello_world.js create mode 100644 lib/db/truncate.js create mode 100644 lib/error-handler.js create mode 100644 lib/server.js create mode 100644 lib/validator.js create mode 100644 package.json create mode 100644 resources/hello-world/handlers.js create mode 100644 resources/hello-world/model.js create mode 100644 resources/hello-world/routes.js create mode 100644 resources/hello-world/schemas.js create mode 100644 test/fixtures/hello-world.js create mode 100644 test/fixtures/index.js create mode 100644 test/resources/hello-world.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0611608 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,json,jsx,css}] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.env b/.env new file mode 100644 index 0000000..ee5d3d3 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +NODE_ENV=development +DATABASE_URL=mysql://nodebr:nodebr@localhost/nodebr +PORT=8080 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..049ab83 --- /dev/null +++ b/.gitignore @@ -0,0 +1,271 @@ + +# Created by https://www.gitignore.io/api/u,node,sublimetext,osx,linux,windows,notepadpp,vim,emacs,webstorm,netbeans + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +#!! ERROR: u is undefined. Use list command to see defined gitignore types !!# + +### SublimeText ### +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon +# Thumbnails +._* +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + + + +### NotepadPP ### +# Notepad++ backups # +*.bak + + +### Vim ### +# swap +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +# session +Session.vim +# temporary +.netrwhist +# auto-generated tag files +tags + + +### Emacs ### +# -*- mode: gitignore; -*- +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + + +### WebStorm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/workspace.xml +.idea/tasks.xml + +# Sensitive or high-churn files: +.idea/dataSources/ +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### WebStorm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +nbdist/ +.nb-gradle/ diff --git a/index.js b/index.js new file mode 100644 index 0000000..91f6570 --- /dev/null +++ b/index.js @@ -0,0 +1,10 @@ +'use strict' + +const server = require('./lib/server') + +// Inicia o servidor na porta especificada pelo environment +server.listen(process.env.PORT, err => { + if (err) throw err + + console.log(`Servidor iniciado na porta ${process.env.PORT}`) +}) diff --git a/lib/db/base.js b/lib/db/base.js new file mode 100644 index 0000000..00282f8 --- /dev/null +++ b/lib/db/base.js @@ -0,0 +1,11 @@ +'use strict' + +/** + * O modelo base para todos os outros models do nosso sistema + * @param {Object} bookshelf Uma instância do Bookshelf + * @return {Object} A mesma instância do Bookshelf com as configurações + * padrões para todos os models + */ +module.exports = bookshelf => { + bookshelf.Model = bookshelf.Model.extend({ uuid: true }) +} diff --git a/lib/db/index.js b/lib/db/index.js new file mode 100644 index 0000000..d8c6301 --- /dev/null +++ b/lib/db/index.js @@ -0,0 +1,25 @@ +'use strict' + +const knex = require('knex')(require('./knexfile')[process.env.NODE_ENV]) +const bookshelf = require('bookshelf')(knex) +const glob = require('glob') +const path = require('path') + +// Registra todos os plugins necessários para o bookshelf +bookshelf.plugin('registry') +bookshelf.plugin('visibility') +bookshelf.plugin('virtuals') +bookshelf.plugin(require('bookshelf-uuid')) +bookshelf.plugin(require('bookshelf-modelbase').pluggable) +bookshelf.plugin(require('bookshelf-bcrypt')) +bookshelf.plugin(require('./base')) +bookshelf.plugin(require('./truncate')) + +const modelsPath = path.resolve(__dirname, '../../resources') + +// Faz o eager loading de todos os módulos +glob.sync(`${modelsPath}/**/model.js`) +.map(require) +.forEach(model => model(bookshelf)) + +module.exports = bookshelf diff --git a/lib/db/knexfile.js b/lib/db/knexfile.js new file mode 100644 index 0000000..4d7a240 --- /dev/null +++ b/lib/db/knexfile.js @@ -0,0 +1,7 @@ +'use strict' + +// No momento só precisamos de um env configurado para o banco de dados +exports[process.env.NODE_ENV] = { + client: 'mysql2', + connection: process.env.DATABASE_URL +} diff --git a/lib/db/migrations/20161114212251_hello_world.js b/lib/db/migrations/20161114212251_hello_world.js new file mode 100644 index 0000000..7a2d257 --- /dev/null +++ b/lib/db/migrations/20161114212251_hello_world.js @@ -0,0 +1,24 @@ +'use strict' + +exports.up = knex => { + // Apenas roda esta migration se estivermos em um ambiente de desenvolvimento + if (process.env.NODE_ENV === 'development') { + return knex.schema.createTableIfNotExists('hello_world', table => { + table.uuid('id').primary() + table.string('nome') + table.string('sobrenome') + table.string('email') + table.timestamps() + }) + } else { + return Promise.resolve() + } +} + +exports.down = knex => { + if (process.env.NODE_ENV === 'development') { + return knex.schema.dropTableIfExists('hello_world') + } else { + return Promise.resolve() + } +} diff --git a/lib/db/truncate.js b/lib/db/truncate.js new file mode 100644 index 0000000..2e4b29e --- /dev/null +++ b/lib/db/truncate.js @@ -0,0 +1,25 @@ +'use strict' + +/** + * Cria uma função no Bookshelf para zerar o banco de dados + * @param {Object} bookshelf Uma instância do Bookshelf + */ +module.exports = bookshelf => { + const { knex } = bookshelf + + bookshelf.truncate = () => { + // Desabilita este comando em qualquer outro ambiante que não seja desenvolvimento + if (process.env.NODE_ENV === 'production') { + return Promise.reject(new Error('Você não pode executar o truncate neste ambiente')) + } + + // Constrói + const commands = Object.keys(bookshelf._models) + .map(key => bookshelf._models[key].prototype.tableName) + .map(tableName => `TRUNCATE TABLE ${tableName};`) + + return knex.raw('SET FOREIGN_KEY_CHECKS = 0;') + .then(() => knex.raw(commands.join(`\n`))) + .then(() => knex.raw('SET FOREIGN_KEY_CHECKS = 1;')) + } +} diff --git a/lib/error-handler.js b/lib/error-handler.js new file mode 100644 index 0000000..869bc67 --- /dev/null +++ b/lib/error-handler.js @@ -0,0 +1,29 @@ +'use strict' + +/** + * Um middleware para captura de erros + * @param {Error} err O erro gerado em um middleware anterior + * @param {Object} req Uma interface de requisição do Express + * @param {Object} res Uma interface de resposta do Express + * @param {Function} next Um callback caso necessário + */ +module.exports = (err, req, res, next) => { + // Verifica se já não houve a resposta + if (res.headersSent) { + return next(err) + } + + // Verifica os tipos de erros que podemos ter + if (err.isJoi) { + res.status(422).send({ + error: 'ValidationError', + message: err.details[0].message, + path: err.details[0].path, + type: err.details[0].type + }) + } else { + // Caso nenhum tipo de erro seja encontrado então é um erro no servidor + res.status(500).send({ error: 'InternalServerError' }) + console.error(err.stack) + } +} diff --git a/lib/server.js b/lib/server.js new file mode 100644 index 0000000..25e180a --- /dev/null +++ b/lib/server.js @@ -0,0 +1,29 @@ +'use strict' + +const express = require('express') +const glob = require('glob') +const path = require('path') +const helmet = require('helmet') + +// Middlewares customizados +const errorHandler = require('./error-handler') + +// Injeta cada rota na nossa app +const app = express() + +// Adiciona todos os middlewares necessários +app.use(helmet()) + +// Faz o require de todas as rotas dentro dos resources e injeta automaticamente +// na nossa app, assim não é necessário fazer o require um por um +const resourcePath = path.resolve(__dirname, '../resources') + +glob.sync(`${resourcePath}/**/routes.js`) +.map(require) +.forEach(route => route && app.use(route)) + +// O gerenciador de erros deve vir por último +app.use(errorHandler) + +// Bootstrapping finalizado, agora é seguro exportar a instância da nossa app +module.exports = app diff --git a/lib/validator.js b/lib/validator.js new file mode 100644 index 0000000..50eca3c --- /dev/null +++ b/lib/validator.js @@ -0,0 +1,41 @@ +'use strict' + +const Joi = require('joi') + +/** + * Um middleware para validar a request antes de chegar no handler + * @param {Object} schemas Um objeto onde cada chave representa a chave da + * requisição que deve ser validada + * @return {Function} Uma função que pode ser utilizada como middleware + */ +module.exports = schemas => (req, res, next) => { + // Uma sinaleira simples guardando o número de validações assíncronas que + // resta fazer antes de chamar o callback + let validations = Object.keys(schemas).length + let nextWasCalled = false + + Object.keys(schemas).forEach(schema => { + Joi.validate(req[schema], schemas[schema], (err, value) => { + // Se houver um erro chame o callback com este erro, mas tenha certeza + // que irá chamar apenas uma vez + if (err && !nextWasCalled) { + nextWasCalled = true + return next(err) + } else { + // Faça o monkeypatch da requisição para evitar que campos não + // validados continuem pela pileline do Express + req[schema] = value + + // Diminui um elemento na sinaleira + --validations + + // Se a sinaleira for 0 significa que todas as validações já voltaram + // agora basta verificar se o next ainda não foi chamado (em caso de + // erro) e passar para o próximo middleware + if (validations === 0 && !nextWasCalled) { + next() + } + } + }) + }) +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..abfa1b5 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "nodebr", + "version": "1.0.0", + "description": "Este repositório refere-se ao backend da comunidade NodeBR hospedada no website https://nodebr.org.", + "main": "index.js", + "scripts": { + "test": "standard && lab --verbose --colors --assert code --ignore __core-js_shared__", + "test-cov": "npm test -- -r console -o stdout -r html -o coverage/coverage.html -r lcov -o coverage/lcov.info", + "knex": "knex --knexfile ./lib/db/knexfile.js", + "start": "node index.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodebr/nodebr.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/nodebr/nodebr/issues" + }, + "homepage": "https://github.com/nodebr/nodebr#readme", + "dependencies": { + "body-parser": "^1.15.2", + "bookshelf": "^0.10.2", + "bookshelf-bcrypt": "^2.1.0", + "bookshelf-modelbase": "^2.10.1", + "bookshelf-uuid": "^1.0.0", + "co": "^4.6.0", + "express": "^4.14.0", + "glob": "^7.1.1", + "helmet": "^3.1.0", + "joi": "^9.2.0", + "knex": "^0.12.6", + "mysql2": "^1.1.1" + }, + "devDependencies": { + "code": "^4.0.0", + "lab": "^11.2.1", + "standard": "^8.5.0", + "supertest": "^2.0.1" + } +} diff --git a/resources/hello-world/handlers.js b/resources/hello-world/handlers.js new file mode 100644 index 0000000..134cc99 --- /dev/null +++ b/resources/hello-world/handlers.js @@ -0,0 +1,25 @@ +'use strict' + +const db = require('../../lib/db') +const HelloWorld = db.model('HelloWorld') + +exports.findAll = (req, res) => { + HelloWorld.findAll() + .then(collection => res.send(collection)) +} + +exports.findOne = (req, res) => { + HelloWorld.findById(req.params.id) + .then(helloWorld => res.send(helloWorld)) +} + +exports.remove = (req, res) => { + HelloWorld.forge({ id: req.params.id }) + .destroy() + .then(() => res.send({ success: true })) +} + +exports.create = (req, res) => { + HelloWorld.create(req.body) + .then(helloWorld => res.send(helloWorld)) +} diff --git a/resources/hello-world/model.js b/resources/hello-world/model.js new file mode 100644 index 0000000..90b91e5 --- /dev/null +++ b/resources/hello-world/model.js @@ -0,0 +1,9 @@ +'use strict' + +/** + * Cria o modelo HelloWorld + * @param {Function} bookshelf Uma instância do Bookshelf + */ +module.exports = bookshelf => bookshelf.model('HelloWorld', { + tableName: 'hello_world' +}) diff --git a/resources/hello-world/routes.js b/resources/hello-world/routes.js new file mode 100644 index 0000000..860e7a4 --- /dev/null +++ b/resources/hello-world/routes.js @@ -0,0 +1,25 @@ +'use strict' + +const express = require('express') +const bodyParser = require('body-parser') + +const validator = require('../../lib/validator') +const schemas = require('./schemas') +const handlers = require('./handlers') +const router = express.Router() + +router.get('/hello-world', handlers.findAll) +router.get('/hello-world/:id', handlers.findOne) + +router.post('/hello-world', + bodyParser.json(), + validator({ body: schemas.create }), + handlers.create) + +router.delete('/hello-world/:id', handlers.remove) + +// Somente exportamos esta rota caso o ambiente for de desenvolvimento +// pois não queremos que a mesma esteja disponível em produção +if (process.env.NODE_ENV !== 'production') { + module.exports = router +} diff --git a/resources/hello-world/schemas.js b/resources/hello-world/schemas.js new file mode 100644 index 0000000..5f5f6fe --- /dev/null +++ b/resources/hello-world/schemas.js @@ -0,0 +1,17 @@ +'use strict' + +const Joi = require('joi') + +exports.model = Joi.object({ + id: Joi.string().uuid(), + nome: Joi.string(), + sobrenome: Joi.string(), + email: Joi.string().email() +}) + +exports.create = exports.model.concat(Joi.object({ + id: Joi.forbidden(), + nome: Joi.required(), + sobrenome: Joi.required(), + email: Joi.required() +})) diff --git a/test/fixtures/hello-world.js b/test/fixtures/hello-world.js new file mode 100644 index 0000000..ba68584 --- /dev/null +++ b/test/fixtures/hello-world.js @@ -0,0 +1,28 @@ +'use strict' + +const { knex } = require('../../lib/db') + +/** + * Insere dois registros na tabela hello_world + * @return {Promise} Uma promise que resolve quando os registros forem inseridos + */ +exports.insertMultiple = () => { + return knex('hello_world').insert([ + { + id: 'ae5c4ed1-916b-4193-bb1c-2f278a4e0243', + nome: 'João', + sobrenome: 'Ninguém', + email: 'joao_ninguem@gmail.com', + created_at: new Date(), + updated_at: new Date() + }, + { + id: '4eb8065f-4483-4877-bfb5-147eb8d2766c', + nome: 'Alan', + sobrenome: 'Hoffmeister', + email: 'alanhoffmeister@gmail.com', + created_at: new Date(), + updated_at: new Date() + } + ]) +} diff --git a/test/fixtures/index.js b/test/fixtures/index.js new file mode 100644 index 0000000..81efb5f --- /dev/null +++ b/test/fixtures/index.js @@ -0,0 +1,15 @@ +'use strict' + +const fs = require('fs') +const path = require('path') + +// Faz o require de todas as fixtures do projeto, assim não precisamos fazer +// o require de cada arquivo individualmente +module.exports = fs.readdirSync(__dirname) +.filter(file => file !== 'index.js') +.reduce((prev, curr) => { + const key = curr.replace(/\.js$/i, '') + prev[key] = require(path.resolve(__dirname, curr)) + + return prev +}, {}) diff --git a/test/resources/hello-world.js b/test/resources/hello-world.js new file mode 100644 index 0000000..38340a2 --- /dev/null +++ b/test/resources/hello-world.js @@ -0,0 +1,106 @@ +'use strict' + +const lab = exports.lab = require('lab').script() +const expect = require('code').expect +const co = require('co') +const request = require('supertest') + +const server = require('../../lib/server') +const db = require('../../lib/db') +const HelloWorld = db.model('HelloWorld') +const fixtures = require('../fixtures') + +const ENDPOINT = '/hello-world' + +lab.describe('hello-world', () => { + lab.beforeEach(() => db.truncate()) + + lab.describe(`POST ${ENDPOINT}`, () => { + lab.test('não deve aceitar um payload que não esteja de acordo com o schema', co.wrap(function * () { + const req = yield request(server) + .post(ENDPOINT) + .send({ hello: 'World' }) + + expect(req.statusCode).to.equal(422) + expect(req.res.body.error).to.equal('ValidationError') + })) + + lab.test('deve criar uma nova linha no banco de dados', co.wrap(function * () { + const req = yield request(server) + .post(ENDPOINT) + .send({ + nome: 'Alan', + sobrenome: 'Hoffmeister', + email: 'alanhoffmeister@gmail.com' + }) + + expect(req.statusCode).to.equal(200) + expect(req.res.body.nome).to.equal('Alan') + expect(req.res.body.sobrenome).to.equal('Hoffmeister') + expect(req.res.body.email).to.equal('alanhoffmeister@gmail.com') + + const data = yield HelloWorld.findAll() + expect(data).to.have.length(1) + expect(data.at(0).toJSON()).to.contain({ + id: req.res.body.id, + nome: 'Alan', + sobrenome: 'Hoffmeister', + email: 'alanhoffmeister@gmail.com' + }) + })) + }) + + lab.describe(`GET ${ENDPOINT}`, () => { + lab.test('deve retornar todas as linhas do banco de dados', co.wrap(function * () { + yield fixtures['hello-world'].insertMultiple() + + const req = yield request(server) + .get(ENDPOINT) + + expect(req.statusCode).to.equal(200) + expect(req.res.body).to.be.an.array() + expect(req.res.body).to.have.length(2) + expect(req.res.body.map(item => item.id)).to.contain([ + 'ae5c4ed1-916b-4193-bb1c-2f278a4e0243', + '4eb8065f-4483-4877-bfb5-147eb8d2766c' + ]) + })) + }) + + lab.describe(`GET ${ENDPOINT}/:id`, () => { + lab.test('deve retornar a linha selecionada', co.wrap(function * () { + yield fixtures['hello-world'].insertMultiple() + + const req = yield request(server) + .get(`${ENDPOINT}/ae5c4ed1-916b-4193-bb1c-2f278a4e0243`) + + expect(req.statusCode).to.equal(200) + expect(req.res.body).to.be.an.object() + expect(req.res.body).to.contain({ + id: 'ae5c4ed1-916b-4193-bb1c-2f278a4e0243', + nome: 'João', + sobrenome: 'Ninguém', + email: 'joao_ninguem@gmail.com' + }) + })) + }) + + lab.describe(`DELETE ${ENDPOINT}/:id`, () => { + lab.test('deve remover do banco de dados a linha selecionada', co.wrap(function * () { + yield fixtures['hello-world'].insertMultiple() + + const req = yield request(server) + .delete(`${ENDPOINT}/ae5c4ed1-916b-4193-bb1c-2f278a4e0243`) + + expect(req.statusCode).to.equal(200) + expect(req.res.body).to.be.an.object() + expect(req.res.body).to.equal({ success: true }) + + const data = yield HelloWorld.findById('ae5c4ed1-916b-4193-bb1c-2f278a4e0243', { + require: false + }) + + expect(data).to.not.exist() + })) + }) +}) From a834cfa9f5b7b622a617220862a1627e2c69d0a8 Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Mon, 14 Nov 2016 23:41:35 -0200 Subject: [PATCH 03/25] =?UTF-8?q?Adiciona=20integra=C3=A7=C3=A3o=20com=20o?= =?UTF-8?q?=20CircleCI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- circle.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 circle.yml diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..60cd69a --- /dev/null +++ b/circle.yml @@ -0,0 +1,17 @@ +general: + branches: + only: + - backend +machine: + services: + - mysql + environment: + DATABASE_URL: mysql://ubuntu@localhost/circle_test + NODE_ENV: test + node: + version: v6.9 +test: + override: + - npm run test-cov + pre: + - npm run knex -- migrate:latest From 1c2265b04dba874829b08fbeed1695d90431e410 Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Mon, 14 Nov 2016 23:49:39 -0200 Subject: [PATCH 04/25] =?UTF-8?q?Deve=20rodar=20a=20migra=C3=A7=C3=A3o=20d?= =?UTF-8?q?e=20hello=5Fworld=20quando=20estiver=20em=20testes=20tamb=C3=A9?= =?UTF-8?q?m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/db/migrations/20161114212251_hello_world.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/db/migrations/20161114212251_hello_world.js b/lib/db/migrations/20161114212251_hello_world.js index 7a2d257..bead55c 100644 --- a/lib/db/migrations/20161114212251_hello_world.js +++ b/lib/db/migrations/20161114212251_hello_world.js @@ -2,7 +2,7 @@ exports.up = knex => { // Apenas roda esta migration se estivermos em um ambiente de desenvolvimento - if (process.env.NODE_ENV === 'development') { + if (process.env.NODE_ENV !== 'production') { return knex.schema.createTableIfNotExists('hello_world', table => { table.uuid('id').primary() table.string('nome') @@ -16,7 +16,7 @@ exports.up = knex => { } exports.down = knex => { - if (process.env.NODE_ENV === 'development') { + if (process.env.NODE_ENV !== 'production') { return knex.schema.dropTableIfExists('hello_world') } else { return Promise.resolve() From ddeb3d79ad832011dd92b190df5dd9d2d0091ea8 Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Tue, 15 Nov 2016 00:10:15 -0200 Subject: [PATCH 05/25] Habilita o CodeClimate --- .codeclimate.yml | 15 +++++++++++++++ README.md | 2 ++ circle.yml | 2 ++ package.json | 2 ++ 4 files changed, 21 insertions(+) create mode 100644 .codeclimate.yml diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..c58aaf1 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,15 @@ +JavaScript: true +engines: + eslint: + enabled: true + duplication: + enabled: true + config: + languages: + - javascript +ratings: + paths: + - resources/**/* + - lib/**/* +exclude_paths: +- test/**/* diff --git a/README.md b/README.md index 501720f..b3fd693 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Backend NodeBR +![](https://circleci.com/gh/nodebr/nodebr/tree/backend.svg?style=shield&circle-token=b074cef2067115bcb93a37b1645cdedf7db96938) + Este repositório refere-se ao backend da comunidade NodeBR hospedada no website https://nodebr.org. diff --git a/circle.yml b/circle.yml index 60cd69a..5647583 100644 --- a/circle.yml +++ b/circle.yml @@ -13,5 +13,7 @@ machine: test: override: - npm run test-cov + - npm install -g codeclimate-test-reporter + - codeclimate-test-reporter < coverage/lcov.info pre: - npm run knex -- migrate:latest diff --git a/package.json b/package.json index abfa1b5..53aa1ab 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ }, "devDependencies": { "code": "^4.0.0", + "eslint-config-standard": "^6.2.1", + "eslint-plugin-standard": "^2.0.1", "lab": "^11.2.1", "standard": "^8.5.0", "supertest": "^2.0.1" From 48d02e8a8592ab20112a46c0be014f26971fb3c1 Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Tue, 15 Nov 2016 00:25:29 -0200 Subject: [PATCH 06/25] Remove o CodeClimate e adiciona o CodeCov --- .codeclimate.yml | 15 --------------- README.md | 1 + circle.yml | 4 ++-- 3 files changed, 3 insertions(+), 17 deletions(-) delete mode 100644 .codeclimate.yml diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index c58aaf1..0000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,15 +0,0 @@ -JavaScript: true -engines: - eslint: - enabled: true - duplication: - enabled: true - config: - languages: - - javascript -ratings: - paths: - - resources/**/* - - lib/**/* -exclude_paths: -- test/**/* diff --git a/README.md b/README.md index b3fd693..257fc16 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Backend NodeBR ![](https://circleci.com/gh/nodebr/nodebr/tree/backend.svg?style=shield&circle-token=b074cef2067115bcb93a37b1645cdedf7db96938) +[![Code Climate](https://codeclimate.com/github/nodebr/nodebr/badges/gpa.svg)](https://codeclimate.com/github/nodebr/nodebr) Este repositório refere-se ao backend da comunidade NodeBR hospedada no website https://nodebr.org. diff --git a/circle.yml b/circle.yml index 5647583..8240c1e 100644 --- a/circle.yml +++ b/circle.yml @@ -13,7 +13,7 @@ machine: test: override: - npm run test-cov - - npm install -g codeclimate-test-reporter - - codeclimate-test-reporter < coverage/lcov.info + post: + - bash <(curl -s https://codecov.io/bash) pre: - npm run knex -- migrate:latest From 0dc9b999ca6f65edd83e83bd60caaad0f1721ff3 Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Tue, 15 Nov 2016 00:32:54 -0200 Subject: [PATCH 07/25] Adiciona a Badge do CodeCov --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 257fc16..987bb12 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Backend NodeBR ![](https://circleci.com/gh/nodebr/nodebr/tree/backend.svg?style=shield&circle-token=b074cef2067115bcb93a37b1645cdedf7db96938) -[![Code Climate](https://codeclimate.com/github/nodebr/nodebr/badges/gpa.svg)](https://codeclimate.com/github/nodebr/nodebr) +[![codecov](https://codecov.io/gh/nodebr/nodebr/branch/backend/graph/badge.svg)](https://codecov.io/gh/nodebr/nodebr/branch/backend) Este repositório refere-se ao backend da comunidade NodeBR hospedada no website https://nodebr.org. From 3bf8085011ad0b53c321f1d368113ba116c3518a Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Tue, 15 Nov 2016 01:46:03 -0200 Subject: [PATCH 08/25] =?UTF-8?q?Adicionando=20link=20para=20a=20documenta?= =?UTF-8?q?=C3=A7=C3=A3o=20no=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 987bb12..fa7f8bc 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,13 @@ Este repositório refere-se ao backend da comunidade NodeBR hospedada no website https://nodebr.org. +### Documentação + +Para que você possa contribuir e desenvolver de forma ágil e sem dúvidas +conselhamos que você leia a documentação do projeto disponível no link abaixo. + +https://github.com/nodebr/nodebr/wiki/Backend + ### Canais de ajuda Antes de iniciar sua contribuição é recomendável que você acesse nosso canal no From 3a052233d09475e5ba022fbb7cc98e6199f4e308 Mon Sep 17 00:00:00 2001 From: bsanches Date: Mon, 14 Nov 2016 22:39:27 -0800 Subject: [PATCH 09/25] Load environment variables from `.env` with `dotenv` module --- index.js | 2 ++ lib/db/knexfile.js | 2 ++ package.json | 1 + 3 files changed, 5 insertions(+) diff --git a/index.js b/index.js index 91f6570..e1e5bce 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,7 @@ 'use strict' +require('dotenv').config() + const server = require('./lib/server') // Inicia o servidor na porta especificada pelo environment diff --git a/lib/db/knexfile.js b/lib/db/knexfile.js index 4d7a240..262a734 100644 --- a/lib/db/knexfile.js +++ b/lib/db/knexfile.js @@ -1,5 +1,7 @@ 'use strict' +require('dotenv').config({ path: __dirname + '/../../.env' }) + // No momento só precisamos de um env configurado para o banco de dados exports[process.env.NODE_ENV] = { client: 'mysql2', diff --git a/package.json b/package.json index 53aa1ab..5cbc30f 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "bookshelf-modelbase": "^2.10.1", "bookshelf-uuid": "^1.0.0", "co": "^4.6.0", + "dotenv": "^2.0.0", "express": "^4.14.0", "glob": "^7.1.1", "helmet": "^3.1.0", From 8f66e1981bd5f9526142014abbbff3c1be1c90e2 Mon Sep 17 00:00:00 2001 From: bsanches Date: Mon, 14 Nov 2016 22:39:55 -0800 Subject: [PATCH 10/25] Add steps necessary to run the app to the README --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 987bb12..d43ac38 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,23 @@ Este repositório refere-se ao backend da comunidade NodeBR hospedada no website https://nodebr.org. +### Setup + +1. Instale `mysql` e crie o database / user. + +```mysql +$ mysql -uroot +mysql> create database nodebr; +mysql> create user 'nodebr'@'localhost' identified by 'nodebr'; +mysql> grant all privileges on nodebr.* to 'nodebr'@'localhost'; +``` + +2. Rode as migrations + +```sh +$ npm run knex migrate:latest +``` + ### Canais de ajuda Antes de iniciar sua contribuição é recomendável que você acesse nosso canal no From ad80c85e50de410f113a62f49426e5e4e3b3e9c1 Mon Sep 17 00:00:00 2001 From: bsanches Date: Mon, 14 Nov 2016 22:44:01 -0800 Subject: [PATCH 11/25] add `morgan` to print request logs --- lib/server.js | 4 ++++ package.json | 1 + 2 files changed, 5 insertions(+) diff --git a/lib/server.js b/lib/server.js index 25e180a..9fda91b 100644 --- a/lib/server.js +++ b/lib/server.js @@ -4,6 +4,7 @@ const express = require('express') const glob = require('glob') const path = require('path') const helmet = require('helmet') +const morgan = require('morgan') // Middlewares customizados const errorHandler = require('./error-handler') @@ -14,6 +15,9 @@ const app = express() // Adiciona todos os middlewares necessários app.use(helmet()) +// Print request logs no console +app.use(morgan('combined')) + // Faz o require de todas as rotas dentro dos resources e injeta automaticamente // na nossa app, assim não é necessário fazer o require um por um const resourcePath = path.resolve(__dirname, '../resources') diff --git a/package.json b/package.json index 5cbc30f..7412e5c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "helmet": "^3.1.0", "joi": "^9.2.0", "knex": "^0.12.6", + "morgan": "^1.7.0", "mysql2": "^1.1.1" }, "devDependencies": { From 62abd44dd8e7662765db120b67df0472299e286b Mon Sep 17 00:00:00 2001 From: bsanches Date: Mon, 14 Nov 2016 23:22:23 -0800 Subject: [PATCH 12/25] Add `users` resource - This follows `hello-world` structure - there are no `'use strict'` thingies because commonjs is strict by default :P - `email` is not in the specification but probably very important (?) - `password` is currently saved raw, missing bcrypt stuff --- lib/db/migrations/20161114224639_users.js | 22 ++++++++++++++ resources/users/handlers.js | 36 +++++++++++++++++++++++ resources/users/model.js | 4 +++ resources/users/routes.js | 20 +++++++++++++ resources/users/schemas.js | 13 ++++++++ 5 files changed, 95 insertions(+) create mode 100644 lib/db/migrations/20161114224639_users.js create mode 100644 resources/users/handlers.js create mode 100644 resources/users/model.js create mode 100644 resources/users/routes.js create mode 100644 resources/users/schemas.js diff --git a/lib/db/migrations/20161114224639_users.js b/lib/db/migrations/20161114224639_users.js new file mode 100644 index 0000000..8ae7d85 --- /dev/null +++ b/lib/db/migrations/20161114224639_users.js @@ -0,0 +1,22 @@ + +exports.up = knex => { + // Apenas roda esta migration se estivermos em um ambiente de desenvolvimento + if (process.env.NODE_ENV !== 'production') { + return knex.schema.createTableIfNotExists('users', table => { + table.uuid('id').primary() + table.string('username', 20).unique().notNullable() + table.string('password', 60).notNullable() + table.timestamps() + }) + } else { + return Promise.resolve() + } +} + +exports.down = knex => { + if (process.env.NODE_ENV !== 'production') { + return knex.schema.dropTableIfExists('users') + } else { + return Promise.resolve() + } +} diff --git a/resources/users/handlers.js b/resources/users/handlers.js new file mode 100644 index 0000000..7ecdd39 --- /dev/null +++ b/resources/users/handlers.js @@ -0,0 +1,36 @@ + +const db = require('../../lib/db') +const User = db.model('User') + +exports.findAll = (req, res) => { + User.findAll() + .then(users => res.send(users)) +} + +exports.findOne = (req, res) => { + User.findById(req.params.id) + .then(user => res.send(user)) +} + +exports.remove = (req, res) => { + User.forge({ id: req.params.id }) + .destroy() + .then(() => res.send({ success: true })) +} + +exports.create = (req, res) => { + User.create(req.body) + .then(user => res.send(user)) + .catch(err => { + if (err.code === 'ER_DUP_ENTRY') { + return res.status(400).send({ + error: 'Este usuário já existe' + }) + } + + // this might be useful while developing + // but maybe bad in production, should ideally just log it + // and return a fixed code to prevent sending sensitive info + return res.status(400).send(err) + }) +} diff --git a/resources/users/model.js b/resources/users/model.js new file mode 100644 index 0000000..736f66f --- /dev/null +++ b/resources/users/model.js @@ -0,0 +1,4 @@ + +module.exports = bookshelf => bookshelf.model('User', { + tableName: 'users' +}) diff --git a/resources/users/routes.js b/resources/users/routes.js new file mode 100644 index 0000000..663f99d --- /dev/null +++ b/resources/users/routes.js @@ -0,0 +1,20 @@ + +const express = require('express') +const bodyParser = require('body-parser') + +const validator = require('../../lib/validator') +const schemas = require('./schemas') +const handlers = require('./handlers') + +const router = express.Router() + +router.get('/users', handlers.findAll) +router.get('/users/:id', handlers.findOne) + +router.post('/users', + bodyParser.json(), + validator({ body: schemas.create }), + handlers.create) + + +module.exports = router diff --git a/resources/users/schemas.js b/resources/users/schemas.js new file mode 100644 index 0000000..4c1ca13 --- /dev/null +++ b/resources/users/schemas.js @@ -0,0 +1,13 @@ +const Joi = require('joi') + +exports.model = Joi.object({ + id: Joi.string().uuid(), + username: Joi.string(), + password: Joi.string() +}) + +exports.create = exports.model.concat(Joi.object({ + id: Joi.forbidden(), + username: Joi.string().alphanum().min(3).max(20).required(), + password: Joi.string().max(60).required() +})) From 5938eb90330d3ece1468e9721dc759e100c73705 Mon Sep 17 00:00:00 2001 From: bsanches Date: Mon, 14 Nov 2016 23:30:16 -0800 Subject: [PATCH 13/25] lint --- lib/db/knexfile.js | 5 +++-- resources/users/routes.js | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/db/knexfile.js b/lib/db/knexfile.js index 262a734..301990c 100644 --- a/lib/db/knexfile.js +++ b/lib/db/knexfile.js @@ -1,6 +1,7 @@ -'use strict' -require('dotenv').config({ path: __dirname + '/../../.env' }) +const path = require('path') + +require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }) // No momento só precisamos de um env configurado para o banco de dados exports[process.env.NODE_ENV] = { diff --git a/resources/users/routes.js b/resources/users/routes.js index 663f99d..b4e2de9 100644 --- a/resources/users/routes.js +++ b/resources/users/routes.js @@ -16,5 +16,4 @@ router.post('/users', validator({ body: schemas.create }), handlers.create) - module.exports = router From 7b9feeaf613471c356719b805c8c99416a0ef648 Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Tue, 15 Nov 2016 11:59:17 -0200 Subject: [PATCH 14/25] =?UTF-8?q?Arruma=20o=20truncate=20para=20operar=20c?= =?UTF-8?q?om=20m=C3=BAltiplas=20tabelas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #64 --- lib/db/truncate.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/db/truncate.js b/lib/db/truncate.js index 2e4b29e..9f7c783 100644 --- a/lib/db/truncate.js +++ b/lib/db/truncate.js @@ -18,8 +18,10 @@ module.exports = bookshelf => { .map(key => bookshelf._models[key].prototype.tableName) .map(tableName => `TRUNCATE TABLE ${tableName};`) - return knex.raw('SET FOREIGN_KEY_CHECKS = 0;') - .then(() => knex.raw(commands.join(`\n`))) - .then(() => knex.raw('SET FOREIGN_KEY_CHECKS = 1;')) + return knex.transaction(trx => { + return trx.raw('SET FOREIGN_KEY_CHECKS = 0;') + .then(() => Promise.all(commands.map(command => trx.raw(command)))) + .then(() => trx.raw('SET FOREIGN_KEY_CHECKS = 1;')) + }) } } From 2ee0e0066f77477a5ddf257f2397201d2c967eac Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Tue, 15 Nov 2016 12:05:12 -0200 Subject: [PATCH 15/25] =?UTF-8?q?Arruma=20as=20configura=C3=A7=C3=B5es=20d?= =?UTF-8?q?o=20CircleCI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- circle.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/circle.yml b/circle.yml index 8240c1e..3340ce9 100644 --- a/circle.yml +++ b/circle.yml @@ -1,7 +1,3 @@ -general: - branches: - only: - - backend machine: services: - mysql From 40dbfb13227925a1fcba43c03ef89131c6a99454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20B=C3=A9rgamo?= Date: Tue, 15 Nov 2016 12:09:33 -0200 Subject: [PATCH 16/25] =?UTF-8?q?Adequando=20=C3=A0s=20especificoes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 2 - lib/db/knexfile.js | 5 --- lib/db/migrations/20161114224639_users.js | 23 ++++-------- package.json | 1 - resources/users/handlers.js | 25 ++---------- resources/users/model.js | 4 +- resources/users/routes.js | 3 -- resources/users/schemas.js | 4 +- test/fixtures/users.js | 27 +++++++++++++ test/resources/hello-world.js | 2 +- test/resources/users.js | 46 +++++++++++++++++++++++ 11 files changed, 89 insertions(+), 53 deletions(-) create mode 100644 test/fixtures/users.js create mode 100644 test/resources/users.js diff --git a/index.js b/index.js index e1e5bce..91f6570 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,5 @@ 'use strict' -require('dotenv').config() - const server = require('./lib/server') // Inicia o servidor na porta especificada pelo environment diff --git a/lib/db/knexfile.js b/lib/db/knexfile.js index 301990c..9728b6a 100644 --- a/lib/db/knexfile.js +++ b/lib/db/knexfile.js @@ -1,8 +1,3 @@ - -const path = require('path') - -require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }) - // No momento só precisamos de um env configurado para o banco de dados exports[process.env.NODE_ENV] = { client: 'mysql2', diff --git a/lib/db/migrations/20161114224639_users.js b/lib/db/migrations/20161114224639_users.js index 8ae7d85..b441ea5 100644 --- a/lib/db/migrations/20161114224639_users.js +++ b/lib/db/migrations/20161114224639_users.js @@ -1,22 +1,13 @@ exports.up = knex => { - // Apenas roda esta migration se estivermos em um ambiente de desenvolvimento - if (process.env.NODE_ENV !== 'production') { - return knex.schema.createTableIfNotExists('users', table => { - table.uuid('id').primary() - table.string('username', 20).unique().notNullable() - table.string('password', 60).notNullable() - table.timestamps() - }) - } else { - return Promise.resolve() - } + return knex.schema.createTableIfNotExists('users', table => { + table.uuid('id').primary() + table.string('username', 20).unique().notNullable() + table.string('password', 60).notNullable() + table.timestamps() + }) } exports.down = knex => { - if (process.env.NODE_ENV !== 'production') { - return knex.schema.dropTableIfExists('users') - } else { - return Promise.resolve() - } + return knex.schema.dropTableIfExists('users') } diff --git a/package.json b/package.json index 7412e5c..71e4d1d 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "bookshelf-modelbase": "^2.10.1", "bookshelf-uuid": "^1.0.0", "co": "^4.6.0", - "dotenv": "^2.0.0", "express": "^4.14.0", "glob": "^7.1.1", "helmet": "^3.1.0", diff --git a/resources/users/handlers.js b/resources/users/handlers.js index 7ecdd39..d2bb760 100644 --- a/resources/users/handlers.js +++ b/resources/users/handlers.js @@ -2,25 +2,9 @@ const db = require('../../lib/db') const User = db.model('User') -exports.findAll = (req, res) => { - User.findAll() - .then(users => res.send(users)) -} - -exports.findOne = (req, res) => { - User.findById(req.params.id) - .then(user => res.send(user)) -} - -exports.remove = (req, res) => { - User.forge({ id: req.params.id }) - .destroy() - .then(() => res.send({ success: true })) -} - -exports.create = (req, res) => { +exports.create = (req, res, next) => { User.create(req.body) - .then(user => res.send(user)) + .then(user => res.send({ success: true })) .catch(err => { if (err.code === 'ER_DUP_ENTRY') { return res.status(400).send({ @@ -28,9 +12,6 @@ exports.create = (req, res) => { }) } - // this might be useful while developing - // but maybe bad in production, should ideally just log it - // and return a fixed code to prevent sending sensitive info - return res.status(400).send(err) + return next(err) }) } diff --git a/resources/users/model.js b/resources/users/model.js index 736f66f..460de65 100644 --- a/resources/users/model.js +++ b/resources/users/model.js @@ -1,4 +1,6 @@ module.exports = bookshelf => bookshelf.model('User', { - tableName: 'users' + tableName: 'users', + hidden: [ 'password' ], + bcrypt: { field: 'password' } }) diff --git a/resources/users/routes.js b/resources/users/routes.js index b4e2de9..068efd1 100644 --- a/resources/users/routes.js +++ b/resources/users/routes.js @@ -8,9 +8,6 @@ const handlers = require('./handlers') const router = express.Router() -router.get('/users', handlers.findAll) -router.get('/users/:id', handlers.findOne) - router.post('/users', bodyParser.json(), validator({ body: schemas.create }), diff --git a/resources/users/schemas.js b/resources/users/schemas.js index 4c1ca13..941f4ab 100644 --- a/resources/users/schemas.js +++ b/resources/users/schemas.js @@ -8,6 +8,6 @@ exports.model = Joi.object({ exports.create = exports.model.concat(Joi.object({ id: Joi.forbidden(), - username: Joi.string().alphanum().min(3).max(20).required(), - password: Joi.string().max(60).required() + username: Joi.string().token().min(3).max(20).required(), + password: Joi.string().min(8).max(120).required() })) diff --git a/test/fixtures/users.js b/test/fixtures/users.js new file mode 100644 index 0000000..7e179b2 --- /dev/null +++ b/test/fixtures/users.js @@ -0,0 +1,27 @@ +'use strict' + +const { knex } = require('../../lib/db') + +/** + * Insere dois registros na tabela hello_world + * @return {Promise} Uma promise que resolve quando os registros forem inseridos + */ +exports.insertMultiple = () => { + return knex('users').insert([ + { + id: '212dd279-129f-474a-beb2-a1cac605cf48', + username: 'alanhoff', + password: 'awesomePassword', + created_at: new Date(), + updated_at: new Date() + }, + { + id: '6af458a8-df22-4da9-a726-89d14076e220', + username: 'thebergamo', + password: 'awesomePassword', + created_at: new Date(), + updated_at: new Date() + } + ]) +} + diff --git a/test/resources/hello-world.js b/test/resources/hello-world.js index 38340a2..b2565e3 100644 --- a/test/resources/hello-world.js +++ b/test/resources/hello-world.js @@ -13,7 +13,7 @@ const fixtures = require('../fixtures') const ENDPOINT = '/hello-world' lab.describe('hello-world', () => { - lab.beforeEach(() => db.truncate()) + lab.beforeEach(() => db.knex('hello_world').truncate()) lab.describe(`POST ${ENDPOINT}`, () => { lab.test('não deve aceitar um payload que não esteja de acordo com o schema', co.wrap(function * () { diff --git a/test/resources/users.js b/test/resources/users.js new file mode 100644 index 0000000..be0336b --- /dev/null +++ b/test/resources/users.js @@ -0,0 +1,46 @@ +'use strict' + +const lab = exports.lab = require('lab').script() +const expect = require('code').expect +const co = require('co') +const request = require('supertest') + +const server = require('../../lib/server') +const db = require('../../lib/db') +const Model = db.model('User') + +const ENDPOINT = '/users' + +lab.describe('users', () => { + lab.beforeEach(() => db.knex('users').truncate()) + + lab.describe(`POST ${ENDPOINT}`, () => { + lab.test('não deve aceitar um payload que não esteja de acordo com o schema', co.wrap(function * () { + const req = yield request(server) + .post(ENDPOINT) + .send({ username: 'p' }) + + expect(req.statusCode).to.equal(422) + expect(req.res.body.error).to.equal('ValidationError') + })) + + lab.test('deve criar uma nova linha no banco de dados', co.wrap(function * () { + const req = yield request(server) + .post(ENDPOINT) + .send({ + username: 'mark', + password: 'awesomePass' + }) + + expect(req.statusCode).to.equal(200) + expect(req.res.body.success).to.equal(true) + + const data = yield Model.findAll() + expect(data).to.have.length(1) + expect(data.at(0).toJSON()).to.contain({ + username: 'mark' + }) + })) + }) +}) + From f26c75cd785fb21aca75699d4a9be3c1fd90faa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20B=C3=A9rgamo?= Date: Tue, 15 Nov 2016 12:12:32 -0200 Subject: [PATCH 17/25] Removendo workarround baseado no PR do alan --- test/resources/hello-world.js | 2 +- test/resources/users.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/resources/hello-world.js b/test/resources/hello-world.js index b2565e3..38340a2 100644 --- a/test/resources/hello-world.js +++ b/test/resources/hello-world.js @@ -13,7 +13,7 @@ const fixtures = require('../fixtures') const ENDPOINT = '/hello-world' lab.describe('hello-world', () => { - lab.beforeEach(() => db.knex('hello_world').truncate()) + lab.beforeEach(() => db.truncate()) lab.describe(`POST ${ENDPOINT}`, () => { lab.test('não deve aceitar um payload que não esteja de acordo com o schema', co.wrap(function * () { diff --git a/test/resources/users.js b/test/resources/users.js index be0336b..453ac7d 100644 --- a/test/resources/users.js +++ b/test/resources/users.js @@ -12,7 +12,7 @@ const Model = db.model('User') const ENDPOINT = '/users' lab.describe('users', () => { - lab.beforeEach(() => db.knex('users').truncate()) + lab.beforeEach(() => db.truncate()) lab.describe(`POST ${ENDPOINT}`, () => { lab.test('não deve aceitar um payload que não esteja de acordo com o schema', co.wrap(function * () { From 21a9df0c69f404e6d1bfee5bd5f4479855cbd989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20B=C3=A9rgamo?= Date: Tue, 15 Nov 2016 12:29:04 -0200 Subject: [PATCH 18/25] Removendo setup do readme.md em prol da wiki --- README.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/README.md b/README.md index 85bc47a..fa7f8bc 100644 --- a/README.md +++ b/README.md @@ -6,23 +6,6 @@ Este repositório refere-se ao backend da comunidade NodeBR hospedada no website https://nodebr.org. -### Setup - -1. Instale `mysql` e crie o database / user. - -```mysql -$ mysql -uroot -mysql> create database nodebr; -mysql> create user 'nodebr'@'localhost' identified by 'nodebr'; -mysql> grant all privileges on nodebr.* to 'nodebr'@'localhost'; -``` - -2. Rode as migrations - -```sh -$ npm run knex migrate:latest -``` - ### Documentação Para que você possa contribuir e desenvolver de forma ágil e sem dúvidas From ca8a94644e5c3a519961a00ddd5e14226924253b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20B=C3=A9rgamo?= Date: Tue, 15 Nov 2016 12:34:33 -0200 Subject: [PATCH 19/25] Trocando nome da rota para pt-br --- resources/users/routes.js | 2 +- test/resources/users.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/users/routes.js b/resources/users/routes.js index 068efd1..dd63dac 100644 --- a/resources/users/routes.js +++ b/resources/users/routes.js @@ -8,7 +8,7 @@ const handlers = require('./handlers') const router = express.Router() -router.post('/users', +router.post('/usuario', bodyParser.json(), validator({ body: schemas.create }), handlers.create) diff --git a/test/resources/users.js b/test/resources/users.js index 453ac7d..91f2e95 100644 --- a/test/resources/users.js +++ b/test/resources/users.js @@ -9,7 +9,7 @@ const server = require('../../lib/server') const db = require('../../lib/db') const Model = db.model('User') -const ENDPOINT = '/users' +const ENDPOINT = '/usuario' lab.describe('users', () => { lab.beforeEach(() => db.truncate()) From 9f6f639247c58ec5d837528b7d65f5dc3c43ee10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20B=C3=A9rgamo?= Date: Tue, 15 Nov 2016 12:42:33 -0200 Subject: [PATCH 20/25] Trocando nome da rota para plural --- resources/users/routes.js | 2 +- test/resources/users.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/users/routes.js b/resources/users/routes.js index dd63dac..a5deb78 100644 --- a/resources/users/routes.js +++ b/resources/users/routes.js @@ -8,7 +8,7 @@ const handlers = require('./handlers') const router = express.Router() -router.post('/usuario', +router.post('/usuarios', bodyParser.json(), validator({ body: schemas.create }), handlers.create) diff --git a/test/resources/users.js b/test/resources/users.js index 91f2e95..ebfc8c2 100644 --- a/test/resources/users.js +++ b/test/resources/users.js @@ -9,7 +9,7 @@ const server = require('../../lib/server') const db = require('../../lib/db') const Model = db.model('User') -const ENDPOINT = '/usuario' +const ENDPOINT = '/usuarios' lab.describe('users', () => { lab.beforeEach(() => db.truncate()) From 1715f1f9d796e367cd494597a9a98c7a3c6c5f54 Mon Sep 17 00:00:00 2001 From: bsanches Date: Tue, 15 Nov 2016 10:02:42 -0800 Subject: [PATCH 21/25] chore stuff - linting, remove blank lines etc --- lib/db/migrations/20161114224639_users.js | 19 ++++------- resources/users/handlers.js | 1 - resources/users/model.js | 1 - resources/users/routes.js | 1 - test/fixtures/users.js | 39 ++++++++++------------- test/resources/users.js | 2 -- 6 files changed, 24 insertions(+), 39 deletions(-) diff --git a/lib/db/migrations/20161114224639_users.js b/lib/db/migrations/20161114224639_users.js index b441ea5..c014455 100644 --- a/lib/db/migrations/20161114224639_users.js +++ b/lib/db/migrations/20161114224639_users.js @@ -1,13 +1,8 @@ +exports.up = knex => knex.schema.createTableIfNotExists('users', table => { + table.uuid('id').primary() + table.string('username', 20).unique().notNullable() + table.string('password', 60).notNullable() + table.timestamps() +}) -exports.up = knex => { - return knex.schema.createTableIfNotExists('users', table => { - table.uuid('id').primary() - table.string('username', 20).unique().notNullable() - table.string('password', 60).notNullable() - table.timestamps() - }) -} - -exports.down = knex => { - return knex.schema.dropTableIfExists('users') -} +exports.down = knex => knex.schema.dropTableIfExists('users') diff --git a/resources/users/handlers.js b/resources/users/handlers.js index d2bb760..6bf92bb 100644 --- a/resources/users/handlers.js +++ b/resources/users/handlers.js @@ -1,4 +1,3 @@ - const db = require('../../lib/db') const User = db.model('User') diff --git a/resources/users/model.js b/resources/users/model.js index 460de65..48c7b1e 100644 --- a/resources/users/model.js +++ b/resources/users/model.js @@ -1,4 +1,3 @@ - module.exports = bookshelf => bookshelf.model('User', { tableName: 'users', hidden: [ 'password' ], diff --git a/resources/users/routes.js b/resources/users/routes.js index a5deb78..4ce1cd7 100644 --- a/resources/users/routes.js +++ b/resources/users/routes.js @@ -1,4 +1,3 @@ - const express = require('express') const bodyParser = require('body-parser') diff --git a/test/fixtures/users.js b/test/fixtures/users.js index 7e179b2..a444d1a 100644 --- a/test/fixtures/users.js +++ b/test/fixtures/users.js @@ -1,27 +1,22 @@ -'use strict' - const { knex } = require('../../lib/db') /** - * Insere dois registros na tabela hello_world + * Insere dois registros na tabela `users` * @return {Promise} Uma promise que resolve quando os registros forem inseridos */ -exports.insertMultiple = () => { - return knex('users').insert([ - { - id: '212dd279-129f-474a-beb2-a1cac605cf48', - username: 'alanhoff', - password: 'awesomePassword', - created_at: new Date(), - updated_at: new Date() - }, - { - id: '6af458a8-df22-4da9-a726-89d14076e220', - username: 'thebergamo', - password: 'awesomePassword', - created_at: new Date(), - updated_at: new Date() - } - ]) -} - +exports.insertMultiple = () => knex('users').insert([ + { + id: '212dd279-129f-474a-beb2-a1cac605cf48', + username: 'alanhoff', + password: 'awesomePassword', + created_at: new Date(), + updated_at: new Date() + }, + { + id: '6af458a8-df22-4da9-a726-89d14076e220', + username: 'thebergamo', + password: 'awesomePassword', + created_at: new Date(), + updated_at: new Date() + } +]) diff --git a/test/resources/users.js b/test/resources/users.js index ebfc8c2..0a70f80 100644 --- a/test/resources/users.js +++ b/test/resources/users.js @@ -1,5 +1,3 @@ -'use strict' - const lab = exports.lab = require('lab').script() const expect = require('code').expect const co = require('co') From 92833ad3a6a8cfafe87f1b5333773f004f9a092a Mon Sep 17 00:00:00 2001 From: bsanches Date: Tue, 15 Nov 2016 10:12:59 -0800 Subject: [PATCH 22/25] test duplicate user insertion --- test/resources/users.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/resources/users.js b/test/resources/users.js index 0a70f80..7fedec8 100644 --- a/test/resources/users.js +++ b/test/resources/users.js @@ -39,6 +39,32 @@ lab.describe('users', () => { username: 'mark' }) })) + + lab.test('retorna um erro quando o usuário já existir', co.wrap(function * () { + // first request + yield request(server) + .post(ENDPOINT) + .send({ + username: 'foo', + password: 'foobarbaz' + }) + + const req = yield request(server) + .post(ENDPOINT) + .send({ + username: 'foo', + password: 'foobarbaz' + }) + + expect(req.statusCode).to.equal(400) + expect(req.res.body.error).to.equal('Este usuário já existe') + + const data = yield Model.findAll() + expect(data).to.have.length(1) + expect(data.at(0).toJSON()).to.contain({ + username: 'foo' + }) + })) }) }) From c26265221ebfac524811d7006af937d87976b69c Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Tue, 15 Nov 2016 19:08:59 -0200 Subject: [PATCH 23/25] =?UTF-8?q?Implementa=20a=20sess=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 + lib/async-handler.js | 12 +++ lib/db/drop-dabase.js | 28 +++++++ lib/db/index.js | 1 + lib/error-handler.js | 2 + lib/session.js | 39 +++++++++ package.json | 6 +- resources/hello-world/routes.js | 2 + resources/sessions/handlers.js | 36 +++++++++ resources/sessions/routes.js | 24 ++++++ resources/sessions/schemas.js | 6 ++ test/fixtures/sessions.js | 21 +++++ test/fixtures/users.js | 4 +- test/resources/sessions.js | 137 ++++++++++++++++++++++++++++++++ 14 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 lib/async-handler.js create mode 100644 lib/db/drop-dabase.js create mode 100644 lib/session.js create mode 100644 resources/sessions/handlers.js create mode 100644 resources/sessions/routes.js create mode 100644 resources/sessions/schemas.js create mode 100644 test/fixtures/sessions.js create mode 100644 test/resources/sessions.js diff --git a/.env b/.env index ee5d3d3..411f3a3 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ NODE_ENV=development DATABASE_URL=mysql://nodebr:nodebr@localhost/nodebr PORT=8080 +COOKIE_SECRET=here_be_dragons diff --git a/lib/async-handler.js b/lib/async-handler.js new file mode 100644 index 0000000..cbd8397 --- /dev/null +++ b/lib/async-handler.js @@ -0,0 +1,12 @@ +const co = require('co') + +/** + * Um helper para registrar handlers assíncronos + * @todo Implementar funcionalidade para Promises + * @param {Function} handler Um generator que irá receber a requisição + * @return {Function} Uma função que pode ser usada como handler no Express + */ +module.exports = handler => (req, res, next) => { + co(handler.bind(null, req, res, next)) + .catch(err => next(err)) +} diff --git a/lib/db/drop-dabase.js b/lib/db/drop-dabase.js new file mode 100644 index 0000000..d2c114f --- /dev/null +++ b/lib/db/drop-dabase.js @@ -0,0 +1,28 @@ +/* $lab:coverage:off$ */ +const co = require('co') + +/** + * Cria uma função no Bookshelf para zerar o banco de dados + * @param {Object} bookshelf Uma instância do Bookshelf + */ +module.exports = bookshelf => { + const { knex } = bookshelf + + bookshelf.dropDatabase = co.wrap(function * () { + // Desabilita este comando em qualquer outro ambiante que não seja desenvolvimento + if (process.env.NODE_ENV === 'production') { + return Promise.reject(new Error('Você não pode executar o dropDatabase neste ambiente')) + } + + // Pega todas as tabelas no nosso banco de dados + const result = yield knex.raw('SHOW TABLES;') + const tables = result[0].map(table => table[Object.keys(table)[0]]) + + yield knex.transaction(co.wrap(function * (trx) { + yield knex.raw('SET FOREIGN_KEY_CHECKS = 0;') + yield Promise.all(tables.map(table => knex.raw(`DROP TABLE ${table};`))) + yield knex.raw('SET FOREIGN_KEY_CHECKS = 1;') + })) + }) +} +/* $lab:coverage:on$ */ diff --git a/lib/db/index.js b/lib/db/index.js index d8c6301..31493b4 100644 --- a/lib/db/index.js +++ b/lib/db/index.js @@ -14,6 +14,7 @@ bookshelf.plugin(require('bookshelf-modelbase').pluggable) bookshelf.plugin(require('bookshelf-bcrypt')) bookshelf.plugin(require('./base')) bookshelf.plugin(require('./truncate')) +bookshelf.plugin(require('./drop-dabase')) const modelsPath = path.resolve(__dirname, '../../resources') diff --git a/lib/error-handler.js b/lib/error-handler.js index 869bc67..aac9e73 100644 --- a/lib/error-handler.js +++ b/lib/error-handler.js @@ -21,6 +21,8 @@ module.exports = (err, req, res, next) => { path: err.details[0].path, type: err.details[0].type }) + } else if (err.isBoom) { + res.status(err.output.statusCode).send(err.output.payload) } else { // Caso nenhum tipo de erro seja encontrado então é um erro no servidor res.status(500).send({ error: 'InternalServerError' }) diff --git a/lib/session.js b/lib/session.js new file mode 100644 index 0000000..e51b777 --- /dev/null +++ b/lib/session.js @@ -0,0 +1,39 @@ +const cookieSession = require('cookie-session') + +/** + * Um middleware para checagem de sessão + * @param {Object} [config] Configurações da sessão + * @param {Boolean} [config.restrict=true] Deixar que apenas usuários logados + * acessem o handler + * @return {Function} Uma função que serve de middleware + */ +module.exports = (config = { + restrict: true +}) => { + const session = cookieSession({ + name: 'session', + keys: [process.env.COOKIE_SECRET], + maxAge: 168 * 60 * 60 * 1000, // Uma semana + secure: process.env.NODE_ENV === 'production', + httpOnly: true, + signed: true, + overwrite: true + }) + + return (req, res, next) => { + // Checa a sessão utilizando o middleware cookie-session + session(req, res, err => { + if (err) { + return next(err) + } + + // Caso o acesso seja restrito à usuários logados precisamos verificar + // se a sessão foi lida com sucesso + if ((!req.session || !req.session.user_id) && config.restrict) { + res.status(401).send({ error: 'Unauthorized' }) + } else { + next() + } + }) + } +} diff --git a/package.json b/package.json index 71e4d1d..a7bf8e5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "test": "standard && lab --verbose --colors --assert code --ignore __core-js_shared__", "test-cov": "npm test -- -r console -o stdout -r html -o coverage/coverage.html -r lcov -o coverage/lcov.info", "knex": "knex --knexfile ./lib/db/knexfile.js", - "start": "node index.js" + "start": "node index.js", + "migrate": "npm run knex migrate:latest", + "reset": "node -e \"require('./lib/db').dropDatabase().then(() => process.exit(0), err => { console.error(err.stack); process.exit(1) })\"; npm run migrate" }, "repository": { "type": "git", @@ -26,7 +28,9 @@ "bookshelf-bcrypt": "^2.1.0", "bookshelf-modelbase": "^2.10.1", "bookshelf-uuid": "^1.0.0", + "boom": "^4.2.0", "co": "^4.6.0", + "cookie-session": "^2.0.0-alpha.2", "express": "^4.14.0", "glob": "^7.1.1", "helmet": "^3.1.0", diff --git a/resources/hello-world/routes.js b/resources/hello-world/routes.js index 860e7a4..06a7fe7 100644 --- a/resources/hello-world/routes.js +++ b/resources/hello-world/routes.js @@ -20,6 +20,8 @@ router.delete('/hello-world/:id', handlers.remove) // Somente exportamos esta rota caso o ambiente for de desenvolvimento // pois não queremos que a mesma esteja disponível em produção +/* $lab:coverage:off$ */ if (process.env.NODE_ENV !== 'production') { module.exports = router } +/* $lab:coverage:on$ */ diff --git a/resources/sessions/handlers.js b/resources/sessions/handlers.js new file mode 100644 index 0000000..c376d79 --- /dev/null +++ b/resources/sessions/handlers.js @@ -0,0 +1,36 @@ +const Boom = require('boom') + +const db = require('../../lib/db') +const User = db.model('User') + +exports.create = function * (req, res) { + // Verifica se o usuário já está logado + if (req.session && req.session.user_id) { + throw Boom.badData('Você já está logado') + } + + // Pega no banco de dados o usuário que precisamos + const user = yield User.findOne({ username: req.body.username }) + .catch(User.NotFoundError, () => { + throw Boom.badData('Não foi possível encontrar o usuário') + }) + + // Verifica se a senha está ok e seta a sessão + if (yield user.compare(req.body.password)) { + req.session.user_id = user.id + } else { + throw Boom.badData('Sua senha está incorreta') + } + + res.send({ success: true }) +} + +exports.findOne = (req, res) => { + User.findById(req.session.user_id) + .then((user) => res.send(user)) +} + +exports.remove = (req, res) => { + req.session = null + res.send({ success: true }) +} diff --git a/resources/sessions/routes.js b/resources/sessions/routes.js new file mode 100644 index 0000000..b906d35 --- /dev/null +++ b/resources/sessions/routes.js @@ -0,0 +1,24 @@ +const router = require('express').Router() + +const handlers = require('./handlers') +const schemas = require('./schemas') +const validator = require('../../lib/validator') +const session = require('../../lib/session') +const asyncHandler = require('../../lib/async-handler') +const bodyParser = require('body-parser') + +router.post('/sessions', + session({ restrict: false }), + bodyParser.json(), + validator({ body: schemas.create }), + asyncHandler(handlers.create)) + +router.get('/sessions', + session(), + handlers.findOne) + +router.delete('/sessions', + session(), + handlers.remove) + +module.exports = router diff --git a/resources/sessions/schemas.js b/resources/sessions/schemas.js new file mode 100644 index 0000000..ac011be --- /dev/null +++ b/resources/sessions/schemas.js @@ -0,0 +1,6 @@ +const Joi = require('joi') + +exports.create = Joi.object({ + username: Joi.string().token().min(3).max(20).required(), + password: Joi.string().min(8).max(120).required() +}) diff --git a/test/fixtures/sessions.js b/test/fixtures/sessions.js new file mode 100644 index 0000000..b7263f5 --- /dev/null +++ b/test/fixtures/sessions.js @@ -0,0 +1,21 @@ +const server = require('../../lib/server') +const request = require('supertest') + +/** + * Cria um cookie compatível com a header Cookie para ser usado com o supertest + * @param {String} username O nome do usuário + * @param {String} password A senha do usuário + * @return {Promise} Uma promise que resolve com o cookie de autenticação + */ +exports.cookie = (username, password) => request(server) +.post('/sessions') +.send({ username, password }) +.then(res => { + const cookies = [] + + // Separa o nome de cada cookie e seu valor + cookies.push(res.header['set-cookie'][0].split(';')[0]) + cookies.push(res.header['set-cookie'][1].split(';')[0]) + + return cookies.join(';') +}) diff --git a/test/fixtures/users.js b/test/fixtures/users.js index a444d1a..a53e66f 100644 --- a/test/fixtures/users.js +++ b/test/fixtures/users.js @@ -8,14 +8,14 @@ exports.insertMultiple = () => knex('users').insert([ { id: '212dd279-129f-474a-beb2-a1cac605cf48', username: 'alanhoff', - password: 'awesomePassword', + password: '$2a$12$SQM/UfsYvHrQ0sWWMQHsfOSg6Hfd.fGFQrj8VymlZGzmtr78BIs6i', // password created_at: new Date(), updated_at: new Date() }, { id: '6af458a8-df22-4da9-a726-89d14076e220', username: 'thebergamo', - password: 'awesomePassword', + password: '$2a$12$SQM/UfsYvHrQ0sWWMQHsfOSg6Hfd.fGFQrj8VymlZGzmtr78BIs6i', // password created_at: new Date(), updated_at: new Date() } diff --git a/test/resources/sessions.js b/test/resources/sessions.js new file mode 100644 index 0000000..657a026 --- /dev/null +++ b/test/resources/sessions.js @@ -0,0 +1,137 @@ +const lab = exports.lab = require('lab').script() +const expect = require('code').expect +const co = require('co') +const request = require('supertest') + +const server = require('../../lib/server') +const db = require('../../lib/db') +const fixtures = require('../fixtures') + +const ENDPOINT = '/sessions' + +lab.describe('sessions', () => { + lab.beforeEach(() => db.truncate()) + + lab.describe(`POST ${ENDPOINT}`, () => { + lab.test('deve criar uma sessão', co.wrap(function * () { + yield fixtures.users.insertMultiple() + + const req = yield request(server) + .post(ENDPOINT) + .send({ + username: 'alanhoff', + password: 'password' + }) + + expect(req.statusCode).to.equal(200) + expect(req.headers['set-cookie']).to.have.length(2) + })) + + lab.test('deve validar os dados', co.wrap(function * () { + yield fixtures.users.insertMultiple() + + const req = yield request(server) + .post(ENDPOINT) + .send({ + username: 'alanhoff', + password: '123' + }) + + expect(req.statusCode).to.equal(422) + expect(req.res.body.error).to.equal('ValidationError') + })) + + lab.test('Não deve aceitar usuários logados', co.wrap(function * () { + yield fixtures.users.insertMultiple() + + const req = yield request(server) + .post(ENDPOINT) + .set('Cookie', yield fixtures.sessions.cookie('alanhoff', 'password')) + .send({ + username: 'alanhoff', + password: 'password' + }) + + expect(req.statusCode).to.equal(422) + expect(req.res.body.message).to.equal('Você já está logado') + })) + + lab.test('Não deve aceitar usuários que não existem', co.wrap(function * () { + yield fixtures.users.insertMultiple() + + const req = yield request(server) + .post(ENDPOINT) + .send({ + username: 'alanhoff2', + password: 'password' + }) + + expect(req.statusCode).to.equal(422) + expect(req.res.body.message).to.equal('Não foi possível encontrar o usuário') + })) + + lab.test('Não deve aceitar senhas inválidas', co.wrap(function * () { + yield fixtures.users.insertMultiple() + + const req = yield request(server) + .post(ENDPOINT) + .send({ + username: 'alanhoff', + password: 'invalid_pwd' + }) + + expect(req.statusCode).to.equal(422) + expect(req.res.body.message).to.equal('Sua senha está incorreta') + })) + }) + + lab.describe(`GET ${ENDPOINT}`, () => { + lab.test('deve retornar a sessão', co.wrap(function * () { + yield fixtures.users.insertMultiple() + + const req = yield request(server) + .get(ENDPOINT) + .set('Cookie', yield fixtures.sessions.cookie('alanhoff', 'password')) + + expect(req.statusCode).to.equal(200) + expect(req.res.body.password).to.not.exist() + expect(req.res.body).to.contain({ + username: 'alanhoff', + id: '212dd279-129f-474a-beb2-a1cac605cf48' + }) + })) + + lab.test('não deve retornar a sessão para usuários não autenticados', co.wrap(function * () { + yield fixtures.users.insertMultiple() + + const req = yield request(server) + .get(ENDPOINT) + + expect(req.statusCode).to.equal(401) + expect(req.res.body.error).to.equal('Unauthorized') + })) + }) + + lab.describe(`DELETE ${ENDPOINT}`, () => { + lab.test('deve remover a sessão sessão', co.wrap(function * () { + yield fixtures.users.insertMultiple() + + const req = yield request(server) + .delete(ENDPOINT) + .set('Cookie', yield fixtures.sessions.cookie('alanhoff', 'password')) + + expect(req.headers['set-cookie'][0].split(';')[0]).to.equal('session=') + })) + + lab.test('não deve remover a sessão de usuários não autenticados', co.wrap(function * () { + yield fixtures.users.insertMultiple() + + const req = yield request(server) + .delete(ENDPOINT) + + expect(req.statusCode).to.equal(401) + expect(req.res.body.error).to.equal('Unauthorized') + })) + }) +}) + From 0e25ad2c6c170e3a0b454d277a36cf963c3bb7be Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Tue, 15 Nov 2016 19:16:55 -0200 Subject: [PATCH 24/25] =?UTF-8?q?Adiciona=20a=20vari=C3=A1vel=20de=20ambie?= =?UTF-8?q?nte=20para=20os=20cookies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- circle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/circle.yml b/circle.yml index 3340ce9..58d981f 100644 --- a/circle.yml +++ b/circle.yml @@ -4,6 +4,7 @@ machine: environment: DATABASE_URL: mysql://ubuntu@localhost/circle_test NODE_ENV: test + COOKIE_SECRET: here_be_dragrons node: version: v6.9 test: From e7edd577ceeb5d1926f61303b17b90b91a925a7f Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Tue, 15 Nov 2016 23:20:59 -0200 Subject: [PATCH 25/25] =?UTF-8?q?Modifica=C3=A7=C3=B5es=20adicionais=20par?= =?UTF-8?q?a=20o=20code=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- circle.yml | 2 +- lib/error-handler.js | 6 +++--- lib/session.js | 9 +++++---- package.json | 2 +- resources/sessions/handlers.js | 8 ++++---- scripts/drop-database.js | 8 ++++++++ test/fixtures/sessions.js | 12 +++--------- 7 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 scripts/drop-database.js diff --git a/circle.yml b/circle.yml index 58d981f..5316e4a 100644 --- a/circle.yml +++ b/circle.yml @@ -4,7 +4,7 @@ machine: environment: DATABASE_URL: mysql://ubuntu@localhost/circle_test NODE_ENV: test - COOKIE_SECRET: here_be_dragrons + COOKIE_SECRET: here_be_dragons node: version: v6.9 test: diff --git a/lib/error-handler.js b/lib/error-handler.js index aac9e73..c75c28b 100644 --- a/lib/error-handler.js +++ b/lib/error-handler.js @@ -15,17 +15,17 @@ module.exports = (err, req, res, next) => { // Verifica os tipos de erros que podemos ter if (err.isJoi) { - res.status(422).send({ + return res.status(422).send({ error: 'ValidationError', message: err.details[0].message, path: err.details[0].path, type: err.details[0].type }) } else if (err.isBoom) { - res.status(err.output.statusCode).send(err.output.payload) + return res.status(err.output.statusCode).send(err.output.payload) } else { // Caso nenhum tipo de erro seja encontrado então é um erro no servidor - res.status(500).send({ error: 'InternalServerError' }) console.error(err.stack) + return res.status(500).send({ error: 'InternalServerError' }) } } diff --git a/lib/session.js b/lib/session.js index e51b777..2dd91e7 100644 --- a/lib/session.js +++ b/lib/session.js @@ -13,7 +13,7 @@ module.exports = (config = { const session = cookieSession({ name: 'session', keys: [process.env.COOKIE_SECRET], - maxAge: 168 * 60 * 60 * 1000, // Uma semana + maxAge: 7 * 24 * 60 * 60 * 1000, // Uma semana secure: process.env.NODE_ENV === 'production', httpOnly: true, signed: true, @@ -30,10 +30,11 @@ module.exports = (config = { // Caso o acesso seja restrito à usuários logados precisamos verificar // se a sessão foi lida com sucesso if ((!req.session || !req.session.user_id) && config.restrict) { - res.status(401).send({ error: 'Unauthorized' }) - } else { - next() + return res.status(401).send({ error: 'Unauthorized' }) } + + // Tudo sob controle, podemos executar o handler + next() }) } } diff --git a/package.json b/package.json index a7bf8e5..51b6ea8 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "knex": "knex --knexfile ./lib/db/knexfile.js", "start": "node index.js", "migrate": "npm run knex migrate:latest", - "reset": "node -e \"require('./lib/db').dropDatabase().then(() => process.exit(0), err => { console.error(err.stack); process.exit(1) })\"; npm run migrate" + "reset": "node ./scripts/drop-database; npm run migrate" }, "repository": { "type": "git", diff --git a/resources/sessions/handlers.js b/resources/sessions/handlers.js index c376d79..d1e6d09 100644 --- a/resources/sessions/handlers.js +++ b/resources/sessions/handlers.js @@ -15,13 +15,13 @@ exports.create = function * (req, res) { throw Boom.badData('Não foi possível encontrar o usuário') }) - // Verifica se a senha está ok e seta a sessão - if (yield user.compare(req.body.password)) { - req.session.user_id = user.id - } else { + // Verifica se a senha está ok + if (!(yield user.compare(req.body.password))) { throw Boom.badData('Sua senha está incorreta') } + // Seta a sessão e retorna sucesso + req.session.user_id = user.id res.send({ success: true }) } diff --git a/scripts/drop-database.js b/scripts/drop-database.js new file mode 100644 index 0000000..b6d04ea --- /dev/null +++ b/scripts/drop-database.js @@ -0,0 +1,8 @@ +const db = require('../lib/db') + +db.dropDatabase() +.then(() => process.exit(0)) +.catch(err => { + console.error(err.stack) + process.exit(1) +}) diff --git a/test/fixtures/sessions.js b/test/fixtures/sessions.js index b7263f5..5cd75b0 100644 --- a/test/fixtures/sessions.js +++ b/test/fixtures/sessions.js @@ -10,12 +10,6 @@ const request = require('supertest') exports.cookie = (username, password) => request(server) .post('/sessions') .send({ username, password }) -.then(res => { - const cookies = [] - - // Separa o nome de cada cookie e seu valor - cookies.push(res.header['set-cookie'][0].split(';')[0]) - cookies.push(res.header['set-cookie'][1].split(';')[0]) - - return cookies.join(';') -}) +.then(res => res.header['set-cookie'] + .map(e => e.split(';')[0]) + .join(';'))