diff --git a/config/routes/account.js b/config/routes/account.js new file mode 100644 index 0000000..6276fec --- /dev/null +++ b/config/routes/account.js @@ -0,0 +1,168 @@ +'use strict' + +const mw = require('../middleware.js') +const User = require('../models.js').user +const mail = require('../mail.js') +const env = require('../env/env.js') +const zxcvbn = require('zxcvbn') +const moment = require('moment') +const debug = require('debug')('tracman-routes-account') +const router = require('express').Router() + + +// Confirm email address +router.get('/email/:token', mw.ensureAuth, async (req, res, next) => { + // Check token + if (req.user.emailToken === req.params.token) { + try { + // Set new email + req.user.email = req.user.newEmail + + // Delete token and newEmail + req.user.emailToken = undefined + req.user.newEmail = undefined + + await req.user.save() + + // Report success + req.flash('success', `Your email has been set to ${req.user.email}. `) + res.redirect('/settings') + + } catch (err) { + mw.throwErr(err, req) + res.redirect('/settings') + } + + // Invalid token + } else { + req.flash('danger', 'Email confirmation token is invalid. ') + res.redirect('/settings') + } +}) + +// Set password +router.route('/password') + .all(mw.ensureAuth, (req, res, next) => { + next() + }) + + // Email user a token, proceed at /password/:token + .get( async (req, res, next) => { + // Create token for password change + try { + let [token, expires] = await req.user.createPassToken() + // Figure out expiration time + let expirationTimeString = (req.query.tz) + ? moment(expires).utcOffset(req.query.tz).toDate().toLocaleTimeString(req.acceptsLanguages[0]) + : moment(expires).toDate().toLocaleTimeString(req.acceptsLanguages[0]) + ' UTC' + + // Alert user to check email. + req.flash('success', + `An link has been sent to ${req.user.email}. \ + Click on the link to complete your password change. \ + This link will expire in one hour (${expirationTimeString}). ` + ) + + // Confirm password change request by email. + return await mail.send({ + to: mail.to(req.user), + from: mail.noReply, + subject: 'Request to change your Tracman password', + text: mail.text( + `A request has been made to change your tracman password. \ + If you did not initiate this request, please contact support at keith@tracman.org. \ + \n\nTo change your password, follow this link:\n\ + ${env.url}/account/password/${token}. \n\n\ + This request will expire at ${expirationTimeString}. ` + ), + html: mail.html( + `

A request has been made to change your tracman password. \ + If you did not initiate this request, please contact support at \ + keith@tracman.org.

\ +

To change your password, follow this link:\ +
\ + ${env.url}/account/password/${token}.

\ +

This request will expire at ${expirationTimeString}.

` + ) + }) + + } catch (err) { + mw.throwErr(err, req) + } finally { + res.redirect((req.user) ? '/settings' : '/login') + } + }) + +router.route('/password/:token') + + // Check token + .all( async (req, res, next) => { + debug('/account/password/:token .all() called') + try { + let user = await User + .findOne({'auth.passToken': req.params.token}) + .where('auth.passTokenExpires').gt(Date.now()) + + if (!user) { + debug('Bad token') + req.flash('danger', 'Password reset token is invalid or has expired. ') + res.redirect((req.isAuthenticated) ? '/settings' : '/login') + } else { + debug('setting passwordUser') + res.locals.passwordUser = user + next() + } + + } catch (err) { + mw.throwErr(err, req) + res.redirect('/password') + } + + }) + + // Show password change form + .get((req, res) => { + debug('/account/password/:token .get() called') + res.render('password') + }) + + // Set new password + .post( async (req, res, next) => { + debug('/account/password/:token .post() called') + + // Validate password strength + let zxcvbnResult = zxcvbn(req.body.password) + if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 864000) { // Less than ten days + req.flash( 'danger', + `That password could be cracked in ${zxcvbnResult.crack_times_display.online_no_throttling_10_per_second}! Come up with a more complex password that would take at least 10 days to crack. ` + ) + res.redirect(`/account/password/${req.params.token}`) + } else { + + // Create hashed password and save to db + try { + await res.locals.passwordUser.generateHashedPassword(req.body.password) + + // User changed password + if (req.user) { + debug('User saved password') + req.flash('success', 'Your password has been changed. ') + res.redirect('/settings') + + // New user created password + } else { + debug('New user created password') + req.flash('success', 'Password set. You can use it to log in now. ') + res.redirect('/login') + } + + } catch (err) { + debug('Error creating hashed password and saving to db') + mw.throwErr(err, req) + res.redirect(`/account/password/${req.params.token}`) + } + + } + }) + +module.exports = router