Skip to content

Instantly share code, notes, and snippets.

@evilsoft
Created November 5, 2017 21:05
Show Gist options
  • Save evilsoft/6e74befcb940c03adec01ee588472f05 to your computer and use it in GitHub Desktop.
Save evilsoft/6e74befcb940c03adec01ee588472f05 to your computer and use it in GitHub Desktop.
Using a ReaderT for database interaction
const { validateUser } = require('../data/users')
const {
compose, curry, objOf
} = require('crocks')
module.exports = ({ config, express, jwt, knex }) => {
const { jwtSecret } = config
const router = express.Router()
const setCookie = res => payload => {
res.cookie('token', jwt.sign(payload, jwtSecret))
res.redirect('/')
}
const render = curry(
(res, locals) => res.render('login', locals)
)
router.get('/', (req, res) => {
render(res, {})
})
router.post('/', (req, res) => {
const { body } = req
validateUser({ knex }, body)
.fork(
compose(render(res), objOf('flash')),
setCookie(res)
)
})
router.get('/logout', (req, res) => {
res.redirect('/')
})
return router
}
const Async = require('crocks/Async')
const ReaderT = require('crocks/Reader/ReaderT')
const crypto = require('crypto')
const assign = require('crocks/helpers/assign')
const assoc = require('crocks/helpers/assoc')
const B = require('crocks/combinators/composeB')
const curry = require('crocks/helpers/curry')
const K = require('crocks/combinators/constant')
const head = require('crocks/Maybe/head')
const map = require('crocks/pointfree/map')
const maybeToAsync = require('crocks/Async/maybeToAsync')
const safe = require('crocks/Maybe/safe')
const AsyncReader = ReaderT(Async)
const { ask, liftFn } = AsyncReader
const { fromPromise } = Async
const table = 'users'
const unique = [ 'email', 'userName' ]
// liftQuery :: (a -> Promise b e) -> AsyncReader Object (Async e b)
const liftQuery = fn =>
ask()
.chain(liftFn(fromPromise(fn)))
// mergeEnv :: Object -> Object
const mergeEnv =
assign({ table, unique })
// maybeEqual :: a -> a -> Maybe a
const maybeEqual = curry(
x => safe(y => y === x)
)
// hash :: Object -> String
const hash = ({ salt='', data }) =>
crypto.createHash('md5')
.update(data.concat(salt), 'utf8')
.digest('hex')
// hashPassword :: Record -> Object
const hashPassword = rec => {
const { password: data, salt } = rec
return assoc('password', hash({ data, salt }), rec)
}
// compareHashed :: String -> Object -> Object
const compareHashed = curry(
(hashed, { password, id }) =>
B(map(K({ id })), maybeEqual(password))(hashed)
)
// getCreds :: Creds -> AsyncReader Object (Async e [ Record ])
const getCreds = ({ username }) =>
liftQuery(({ knex, table }) =>
knex(table)
.select([ 'id', 'password', 'salt' ])
.where({ userName: username })
.orWhere({ email: username })
)
// validatePassword :: Object -> Record -> Async e Record
const validatePassword = curry(
(creds, rec) => {
const { salt, id, password: hashed } = rec
const { password } = creds
return Async.of(hashPassword({ id, salt, password }))
.chain(maybeToAsync('Invalid Credentials', compareHashed(hashed)))
}
)
// validateUser :: Object -> Creds -> Async e Record
exports.validateUser = curry(
(env, creds) =>
AsyncReader.of(creds)
.chain(getCreds)
.chain(liftFn(maybeToAsync('Invalid Credentials', head)))
.chain(liftFn(validatePassword(creds)))
.runWith(mergeEnv(env))
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment