diff --git a/package.json b/package.json index 58a353d..dcebf92 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "christmas-community", - "version": "1.12.0", + "version": "1.13.0", "description": "Christmas lists for communities", "main": "main.js", "scripts": { diff --git a/routes/adminSettings/index.js b/routes/adminSettings/index.js index dc9aee5..24c7408 100644 --- a/routes/adminSettings/index.js +++ b/routes/adminSettings/index.js @@ -3,8 +3,8 @@ const bcrypt = require('bcrypt-nodejs'); const express = require('express'); const { nanoid } = require('nanoid') -const SIGNUP_TOKEN_LENGTH = 32 -const SIGNUP_TOKEN_LIFETIME = +const SECRET_TOKEN_LENGTH = 32 +const SECRET_TOKEN_LIFETIME = // One week, approximately. Doesn't need to be perfect. 1000 // milliseconds * 60 // seconds @@ -31,8 +31,8 @@ module.exports = (db) => { admin: false, wishlist: [], - signupToken: nanoid(SIGNUP_TOKEN_LENGTH), - expiry: new Date().getTime() + SIGNUP_TOKEN_LIFETIME + signupToken: nanoid(SECRET_TOKEN_LENGTH), + expiry: new Date().getTime() + SECRET_TOKEN_LIFETIME }); res.redirect(`/admin-settings/edit/${req.body.newUserUsername.trim()}`) @@ -48,8 +48,26 @@ module.exports = (db) => { router.post('/edit/refresh-signup-token/:userToEdit', verifyAuth(), async (req, res) => { if (!req.user.admin) return res.redirect('/'); const doc = await db.get(req.params.userToEdit) - doc.signupToken = nanoid(SIGNUP_TOKEN_LENGTH) - doc.expiry = new Date().getTime() + SIGNUP_TOKEN_LIFETIME + doc.signupToken = nanoid(SECRET_TOKEN_LENGTH) + doc.expiry = new Date().getTime() + SECRET_TOKEN_LIFETIME + await db.put(doc) + return res.redirect(`/admin-settings/edit/${req.params.userToEdit}`) + }); + + router.post('/edit/resetpw/:userToEdit', verifyAuth(), async (req, res) => { + if (!req.user.admin) return res.redirect('/'); + const doc = await db.get(req.params.userToEdit) + doc.pwToken = nanoid(SECRET_TOKEN_LENGTH) + doc.pwExpiry = new Date().getTime() + SECRET_TOKEN_LIFETIME + await db.put(doc) + return res.redirect(`/admin-settings/edit/${req.params.userToEdit}`) + }); + + router.post('/edit/cancelresetpw/:userToEdit', verifyAuth(), async (req, res) => { + if (!req.user.admin) return res.redirect('/'); + const doc = await db.get(req.params.userToEdit) + delete doc.pwToken + delete doc.pwExpiry await db.put(doc) return res.redirect(`/admin-settings/edit/${req.params.userToEdit}`) }); diff --git a/routes/confirm-account/index.js b/routes/confirm-account/index.js index dbe5974..5ba62e0 100644 --- a/routes/confirm-account/index.js +++ b/routes/confirm-account/index.js @@ -5,11 +5,11 @@ module.exports = (db) => { const router = express.Router(); router.get('/:code', async (req, res) => { - const { doc } = (await db.allDocs({ include_docs: true })) + const row = (await db.allDocs({ include_docs: true })) .rows .find(({ doc }) => doc.signupToken === req.params.code) - res.render('confirm-account', { doc }) + res.render('confirm-account', { doc: row ? row.doc : undefined }) }); router.post('/:code', async (req, res) => { diff --git a/routes/index.js b/routes/index.js index 40c3dc1..90e1d0b 100644 --- a/routes/index.js +++ b/routes/index.js @@ -28,6 +28,7 @@ module.exports = ({ db, config }) => { router.use('/login', require('./login')()); router.use('/logout', require('./logout')()); + router.use('/resetpw', require('./resetpw')(db)); router.use('/confirm-account', require('./confirm-account')(db)); router.use('/wishlist', require('./wishlist')(db)); diff --git a/routes/resetpw/index.js b/routes/resetpw/index.js new file mode 100644 index 0000000..d2a8d2f --- /dev/null +++ b/routes/resetpw/index.js @@ -0,0 +1,45 @@ +const bcrypt = require('bcrypt-nodejs'); +const express = require('express'); + +module.exports = (db) => { + const router = express.Router(); + + router.get('/:code', async (req, res) => { + const row = (await db.allDocs({ include_docs: true })) + .rows + .find(({ doc }) => doc.pwToken === req.params.code) + + + res.render('resetpw', { doc: row ? row.doc : undefined }) + }); + + router.post('/:code', async (req, res) => { + const { doc } = (await db.allDocs({ include_docs: true })) + .rows + .find(({ doc }) => doc.pwToken === req.params.code) + + if (doc.expiry < new Date().getTime()) return res.redirect(`/resetpw/${req.params.code}`) + + bcrypt.hash(req.body.password, null, null, async (err, passwordHash) => { + if (err) throw err; + + doc.password = passwordHash + delete doc.pwToken + delete doc.pwExpiry + + await db.put(doc) + + req.login({ _id: doc._id }, err => { + if (err) { + console.log(err) + req.flash('error', err.message) + return res.redirect('/') + } + req.flash('success', `Welcome to ${_CC.config.siteTitle}!`); + res.redirect('/'); + }) + }); + }); + + return router; +}; \ No newline at end of file diff --git a/views/admin-user-edit.pug b/views/admin-user-edit.pug index 9646340..4c48930 100644 --- a/views/admin-user-edit.pug +++ b/views/admin-user-edit.pug @@ -10,7 +10,7 @@ block content .columns .column if user.signupToken - - const link = `${_CC.config.base}confirm-account/${user.signupToken}` + - const signupLink = `${_CC.config.base}confirm-account/${user.signupToken}` .box(style='overflow: hidden;') .columns(style='margin-bottom: 0;') .column.is-narrow(style='padding-bottom: 0;') @@ -30,7 +30,7 @@ block content form(method='POST', action=`${_CC.config.base}admin-settings/edit/refresh-signup-token/${user._id}`) input.button.is-rounded(type='submit', value='Generate New Link') .level-item - a(href=link, style='font-family: monospaced; word-break: break-all;')= link + a(href=signupLink, style='font-family: monospaced; word-break: break-all;')= signupLink h2 Change Name form(action=`${_CC.config.base}admin-settings/edit/rename/${user._id}`, method='POST') .field @@ -42,6 +42,31 @@ block content .field .control input.button.is-primary(type='submit' value='Change Username') + h2(style='margin-bottom: 1em;') Reset Password + if user.pwToken + - const resetLink = `${_CC.config.base}resetpw/${user.pwToken}` + p There is a reset password link for this user. + if user.pwExpiry > new Date().getTime() + span It expires #{_CC.require('moment')(user.pwExpiry).fromNow()} + else + span.has-text-weight-bold.has-text-danger It expired #{_CC.require('moment')(user.pwExpiry).fromNow()} + a(href=resetLink)= resetLink + .columns + .column.is-narrow + form(method='POST', action=`${_CC.config.base}admin-settings/edit/resetpw/${user._id}`) + .field + .control + input.button.is-primary(type='submit' value='Refresh Password Reset Link') + .column.is-narrow + form(method='POST', action=`${_CC.config.base}admin-settings/edit/cancelresetpw/${user._id}`) + .field + .control + input.button.is-info(type='submit' value='Cancel Password Reset Link') + else + form(method='POST', action=`${_CC.config.base}admin-settings/edit/resetpw/${user._id}`) + .field + .control + input.button.is-danger(type='submit' value='Create Password Reset Link') .column.is-narrow h2 Irreversible Deletion form(method='POST', action=`${_CC.config.base}admin-settings/edit/remove/${user._id}`) diff --git a/views/resetpw.pug b/views/resetpw.pug new file mode 100644 index 0000000..da2eb63 --- /dev/null +++ b/views/resetpw.pug @@ -0,0 +1,34 @@ +extends layout.pug + +mixin icon(c, text) + .columns.is-vcentered.is-mobile + .column.is-narrow + span.icon.is-large + i.fa-3x(class=c) + .column #{text} + +block title + if doc + h1 #{config.siteTitle} | Reset Password + else + h1 #{config.siteTitle} | Reset Link Invalid + +block content + if doc + if doc.pwExpiry > new Date().getTime() + +icon('fas fa-smile-beam', `Hello ${doc._id}! Please set your password here.`) + form(method='POST') + .field + label.label Password + .control.has-icons-left + input.input(type='password', name='password', placeholder='pa$$word!') + span.icon.is-small.is-left + i.fas.fa-lock + .field + .control + input.button.is-primary(type='submit' value=`Reset Password`) + else + +icon('fas fa-frown-open', 'Your reset link has expired. Please ask for a new one.') + else + +icon('fas fa-frown-open', "This reset link isn't valid, perhaps the link was canceled or some characters at the end got cut off?") +