Skip to content

Instantly share code, notes, and snippets.

@timwis
Created September 18, 2016 21:09
Show Gist options
  • Save timwis/a942ff08bbfce33ba365dc01ca09a4a7 to your computer and use it in GitHub Desktop.
Save timwis/a942ff08bbfce33ba365dc01ca09a4a7 to your computer and use it in GitHub Desktop.

CouchDB Password Reset

Password reset implementation for CouchDB

Usage

const http = require('http')
const jsonBody = require('body/json')
const PasswordReset = require('couchdb-password-reset')
const Templates = reqiure('couchdb-password-reset/templates')

// Optionally use included templates by providing them basic info
const templates = Templates({
  siteName: 'My Site',
  constructResetURL: (token) => 'http://mysite.com/password-reset' + token,
  moreInfoURL: 'http://mysite.com/about'
})

const passwordReset = PasswordReset({
  dbURL: 'http://localhost:5984/_users',
  postmarkServerToken: 'as0d98fasd0f',
  fromEmail: 'noreply@site.com',
  resetTokenTemplate: templates.resetToken,
  notFoundTemplate: templates.notFound
})

http.createServer((req, res) => {
  switch (req.url) {
    case '/reset/init': return (req, res) => {
      jsonBody(req, (err, body) => {
        const email = body.email
        passwordReset.init(email, (err) => {
          if (err) res.statusCode = err.statusCode
          res.end()
        })
      })
    }
    case '/reset/confirm': return (req, res) => {
      jsonBody(req, (err, body) => {
        const token = body.token
        const password = body.password
        passwordReset.confirm(token, password, (err) => {
          if (err) res.statusCode = err.statusCode
          res.end()
        })
      })
    }
  }
}).listen(8080)
const assert = require('assert')
const uuid = require('node-uuid')
const extend = require('xtend')
const createError = require('create-error')
const nano = require('nano')
const postmark = require('postmark')
const UserError = createError('UserError', {statusCode: 400})
const ServerError = createError('ServerError', {statusCode: 500})
module.exports = PasswordReset
function PasswordReset ({
dbURL,
postmarkServerToken,
fromEmail,
tokenLifespan = 30,
resetTokenSubject = 'Password reset',
resetTokenTemplate,
notFoundSubject = 'Attempted password reset',
notFoundTemplate
}) {
assert(dbURL, 'dbURL must be passed')
assert(fromEmail, 'fromEmail must be passed')
assert(resetTokenTemplate, 'resetTokenTemplate must be passed')
assert(notFoundTemplate, 'notFoundTemplate must be passed')
const db = nano(dbURL)
const emailClient = new postmark.Client(postmarkServerToken)
return {
init: initReset,
confirm: confirmReset
}
function initReset (email, cb) {
if (!email) return cb(new UserError('No email provided'))
// Find user document
const viewOpts = { keys: [email], include_docs: true }
db.view('users', 'byEmail', viewOpts, (err, body) => {
// If something went wrong while finding (*not* thrown if no doc found)
if (err) return cb(new ServerError('Error finding user document'))
if (body.rows.length) {
// Found user document
const token = uuid.v4()
const newUserDoc = extend(body.rows[0].doc)
newUserDoc.metadata.resetToken = {
created: Date.now(),
token
}
// Add reset token to user document
db.insert(newUserDoc, (err, body) => {
if (err) return cb(new ServerError('Error adding reset token to user document'))
const emailConfig = {
From: fromEmail,
To: email,
Subject: resetTokenSubject,
TextBody: resetTokenTemplate(token)
}
// Email reset token to user
emailClient.sendEmail(emailConfig, (err, result) => {
if (err) return cb(new ServerError('Error sending reset email'))
cb() // success
})
})
} else {
// No user found
const emailConfig = {
From: fromEmail,
To: email,
Subject: notFoundSubject,
TextBody: notFoundTemplate()
}
// Email "not found" notice to user
emailClient.sendEmail(emailConfig, (err, result) => {
if (err) return cb(new ServerError('Error sending reset email'))
cb() // success
})
}
})
}
function confirmReset (token, password, cb) {
if (!token || !password) return cb(new UserError('Token or password missing'))
const viewOpts = { keys: [token], include_docs: true }
db.view('users', 'byResetToken', viewOpts, (err, body) => {
if (err) return cb(new ServerError('Error fetching user document'))
if (!body.rows.length || isExpired(body.rows[0].value)) {
return cb(new UserError('Token not found or token is inactive', {statusCode: 404}))
}
// User document found with that token
const newUserDoc = extend(body.rows[0].doc)
newUserDoc.password = password
delete newUserDoc.metadata.resetToken
// Update password and remove reset token from user document
db.insert(newUserDoc, (err, body) => {
if (err) cb(new ServerError('Error saving user document'))
cb() // success
})
})
}
function isExpired (timestamp) {
return (Date.now() - timestamp) / 1000 / 60 > tokenLifespan
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment