Skip to content

Instantly share code, notes, and snippets.

@evilsoft
Created November 5, 2017 21:05

Revisions

  1. evilsoft created this gist Nov 5, 2017.
    39 changes: 39 additions & 0 deletions login.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,39 @@
    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
    }
    83 changes: 83 additions & 0 deletions users.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,83 @@
    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))
    )