v1.13.0: Reset Password

This commit is contained in:
Wingy 2020-10-30 11:15:00 -04:00
parent 10c585d230
commit 22ebde8a0b
7 changed files with 134 additions and 11 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "christmas-community", "name": "christmas-community",
"version": "1.12.0", "version": "1.13.0",
"description": "Christmas lists for communities", "description": "Christmas lists for communities",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View file

@ -3,8 +3,8 @@ const bcrypt = require('bcrypt-nodejs');
const express = require('express'); const express = require('express');
const { nanoid } = require('nanoid') const { nanoid } = require('nanoid')
const SIGNUP_TOKEN_LENGTH = 32 const SECRET_TOKEN_LENGTH = 32
const SIGNUP_TOKEN_LIFETIME = const SECRET_TOKEN_LIFETIME =
// One week, approximately. Doesn't need to be perfect. // One week, approximately. Doesn't need to be perfect.
1000 // milliseconds 1000 // milliseconds
* 60 // seconds * 60 // seconds
@ -31,8 +31,8 @@ module.exports = (db) => {
admin: false, admin: false,
wishlist: [], wishlist: [],
signupToken: nanoid(SIGNUP_TOKEN_LENGTH), signupToken: nanoid(SECRET_TOKEN_LENGTH),
expiry: new Date().getTime() + SIGNUP_TOKEN_LIFETIME expiry: new Date().getTime() + SECRET_TOKEN_LIFETIME
}); });
res.redirect(`/admin-settings/edit/${req.body.newUserUsername.trim()}`) 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) => { router.post('/edit/refresh-signup-token/:userToEdit', verifyAuth(), async (req, res) => {
if (!req.user.admin) return res.redirect('/'); if (!req.user.admin) return res.redirect('/');
const doc = await db.get(req.params.userToEdit) const doc = await db.get(req.params.userToEdit)
doc.signupToken = nanoid(SIGNUP_TOKEN_LENGTH) doc.signupToken = nanoid(SECRET_TOKEN_LENGTH)
doc.expiry = new Date().getTime() + SIGNUP_TOKEN_LIFETIME 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) await db.put(doc)
return res.redirect(`/admin-settings/edit/${req.params.userToEdit}`) return res.redirect(`/admin-settings/edit/${req.params.userToEdit}`)
}); });

View file

@ -5,11 +5,11 @@ module.exports = (db) => {
const router = express.Router(); const router = express.Router();
router.get('/:code', async (req, res) => { router.get('/:code', async (req, res) => {
const { doc } = (await db.allDocs({ include_docs: true })) const row = (await db.allDocs({ include_docs: true }))
.rows .rows
.find(({ doc }) => doc.signupToken === req.params.code) .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) => { router.post('/:code', async (req, res) => {

View file

@ -28,6 +28,7 @@ module.exports = ({ db, config }) => {
router.use('/login', require('./login')()); router.use('/login', require('./login')());
router.use('/logout', require('./logout')()); router.use('/logout', require('./logout')());
router.use('/resetpw', require('./resetpw')(db));
router.use('/confirm-account', require('./confirm-account')(db)); router.use('/confirm-account', require('./confirm-account')(db));
router.use('/wishlist', require('./wishlist')(db)); router.use('/wishlist', require('./wishlist')(db));

45
routes/resetpw/index.js Normal file
View file

@ -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;
};

View file

@ -10,7 +10,7 @@ block content
.columns .columns
.column .column
if user.signupToken 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;') .box(style='overflow: hidden;')
.columns(style='margin-bottom: 0;') .columns(style='margin-bottom: 0;')
.column.is-narrow(style='padding-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}`) 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') input.button.is-rounded(type='submit', value='Generate New Link')
.level-item .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 h2 Change Name
form(action=`${_CC.config.base}admin-settings/edit/rename/${user._id}`, method='POST') form(action=`${_CC.config.base}admin-settings/edit/rename/${user._id}`, method='POST')
.field .field
@ -42,6 +42,31 @@ block content
.field .field
.control .control
input.button.is-primary(type='submit' value='Change Username') 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 .column.is-narrow
h2 Irreversible Deletion h2 Irreversible Deletion
form(method='POST', action=`${_CC.config.base}admin-settings/edit/remove/${user._id}`) form(method='POST', action=`${_CC.config.base}admin-settings/edit/remove/${user._id}`)

34
views/resetpw.pug Normal file
View file

@ -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?")