Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NODE_ENV=development
DATABASE_URL=mysql://nodebr:nodebr@localhost/nodebr
PORT=8080
COOKIE_SECRET=here_be_dragons
1 change: 1 addition & 0 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ machine:
environment:
DATABASE_URL: mysql://ubuntu@localhost/circle_test
NODE_ENV: test
COOKIE_SECRET: here_be_dragons
node:
version: v6.9
test:
Expand Down
12 changes: 12 additions & 0 deletions lib/async-handler.js
Original file line number Diff line number Diff line change
@@ -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))
}
28 changes: 28 additions & 0 deletions lib/db/drop-dabase.js
Original file line number Diff line number Diff line change
@@ -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$ */
1 change: 1 addition & 0 deletions lib/db/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
6 changes: 4 additions & 2 deletions lib/error-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +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) {
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' })
}
}
40 changes: 40 additions & 0 deletions lib/session.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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: 7 * 24 * 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) {
return res.status(401).send({ error: 'Unauthorized' })
}

// Tudo sob controle, podemos executar o handler
next()
})
}
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./scripts/drop-database; npm run migrate"
},
"repository": {
"type": "git",
Expand All @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions resources/hello-world/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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$ */
36 changes: 36 additions & 0 deletions resources/sessions/handlers.js
Original file line number Diff line number Diff line change
@@ -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
if (!(yield user.compare(req.body.password))) {
throw Boom.badData('Sua senha está incorreta')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acho que faz mais sentido checar se não tem password e devolver um throw. Dai estar a sessão caso tudo esteja correto e seguir a vida.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thebergamo não entendi

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!yeld user.compare(req.body.password)) {
    throw Boom.badData('Sua senha está incorreta')
}

Tipo isso ^

}

// Seta a sessão e retorna sucesso
req.session.user_id = user.id
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 })
}
24 changes: 24 additions & 0 deletions resources/sessions/routes.js
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions resources/sessions/schemas.js
Original file line number Diff line number Diff line change
@@ -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()
})
8 changes: 8 additions & 0 deletions scripts/drop-database.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const db = require('../lib/db')

db.dropDatabase()
.then(() => process.exit(0))
.catch(err => {
console.error(err.stack)
process.exit(1)
})
15 changes: 15 additions & 0 deletions test/fixtures/sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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 => res.header['set-cookie']
.map(e => e.split(';')[0])
.join(';'))
4 changes: 2 additions & 2 deletions test/fixtures/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Loading