'use strict' const mw = require('../middleware.js') const mail = require('../mail.js') const User = require('../models.js').user const crypto = require('crypto') const moment = require('moment') const slugify = require('slug') const debug = require('debug')('tracman-routes-auth') const env = require('../env/env.js') module.exports = (app, passport) => { // Methods for success and failure const loginOutcome = { failureRedirect: '/login', failureFlash: true } const loginCallback = (req, res) => { debug(`Login callback called... redirecting to ${req.session.next}`) req.flash(req.session.flashType, req.session.flashMessage) req.session.flashType = undefined req.session.flashMessage = undefined res.redirect(req.session.next || '/map') } const appLoginCallback = (req, res, next) => { debug('appLoginCallback called.') if (req.user) { res.send(req.user) } else { let err = new Error('Unauthorized') err.status = 401 next(err) } } // Login/-out app.route('/login') .get((req, res) => { // Already logged in if (req.isAuthenticated()) { loginCallback(req, res) // Show login page } else { res.render('login') } }) .post(passport.authenticate('local', loginOutcome), loginCallback) app.get('/logout', (req, res) => { req.logout() req.flash('success', `You have been logged out.`) res.redirect(req.session.next || '/') }) // Signup app.route('/signup') .get((req, res) => { res.redirect('/login#signup') }) .post((req, res, next) => { // Send token and alert user function sendToken (user) { debug(`sendToken() called for user ${user.id}`) // Create a password token user.createPassToken((err, token, expires) => { if (err) { debug(`Error creating password token for user ${user.id}!`) mw.throwErr(err, req) res.redirect('/login#signup') } else { debug(`Created password token for user ${user.id} successfully`) // 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' // Email the instructions to continue debug(`Emailing new user ${user.id} at ${user.email} instructions to create a password...`) mail.send({ from: mail.noReply, to: `<${user.email}>`, subject: 'Complete your Tracman registration', text: mail.text(`Welcome to Tracman! \n\nTo complete your registration, follow this link and set your password:\n${env.url}/settings/password/${token}\n\nThis link will expire at ${expirationTimeString}. `), html: mail.html(`

Welcome to Tracman!

To complete your registration, follow this link and set your password:
${env.url}/settings/password/${token}

This link will expire at ${expirationTimeString}.

`) }) .then(() => { debug(`Successfully emailed new user ${user.id} instructions to continue`) req.flash('success', `An email has been sent to ${user.email}. Check your inbox and follow the link to complete your registration. (Your registration link will expire in one hour). `) res.redirect('/login') }) .catch((err) => { debug(`Failed to email new user ${user.id} instructions to continue!`) mw.throwErr(err, req) res.redirect('/login#signup') }) } }) } // Validate email req.checkBody('email', 'Please enter a valid email address.').isEmail() // Check if somebody already has that email debug(`Searching for user with email ${req.body.email}...`) User.findOne({'email': req.body.email}) .then((user) => { // User already exists if (user && user.auth.password) { debug(`User ${user.id} has email ${req.body.email} and has a password`) req.flash('warning', `A user with that email already exists! If you forgot your password, you can reset it here.`) res.redirect('/login#login') next() // User exists but hasn't created a password yet } else if (user) { debug(`User ${user.id} has email ${req.body.email} but doesn't have a password`) // Send another token (or the same one if it hasn't expired) sendToken(user) // Create user } else { debug(`User with email ${req.body.email} doesn't exist; creating one`) user = new User() user.created = Date.now() user.email = req.body.email user.slug = slugify(user.email.substring(0, user.email.indexOf('@'))) // Generate unique slug const slug = new Promise((resolve, reject) => { debug(`Creating new slug for user...`); (function checkSlug (s, cb) { debug(`Checking to see if slug ${s} is taken...`) User.findOne({slug: s}) .then((existingUser) => { // Slug in use: generate a random one and retry if (existingUser) { debug(`Slug ${s} is taken; generating another...`) crypto.randomBytes(6, (err, buf) => { if (err) { debug('Failed to create random bytes for slug!') mw.throwErr(err, req) reject() } if (buf) { checkSlug(buf.toString('hex'), cb) } }) // Unique slug: proceed } else { debug(`Slug ${s} is unique`) cb(s) } }) .catch((err) => { debug('Failed to create slug!') mw.throwErr(err, req) reject() }) })(user.slug, (newSlug) => { debug(`Successfully created slug: ${newSlug}`) user.slug = newSlug resolve() }) }) // Generate sk32 const sk32 = new Promise((resolve, reject) => { debug('Creating sk32 for user...') crypto.randomBytes(32, (err, buf) => { if (err) { debug('Failed to create sk32!') mw.throwErr(err, req) reject() } if (buf) { user.sk32 = buf.toString('hex') debug(`Successfully created sk32: ${user.sk32}`) resolve() } }) }) // Save user and send the token by email Promise.all([slug, sk32]) .then(() => { sendToken(user) }) .catch((err) => { debug('Failed to save user after creating slug and sk32!') mw.throwErr(err, req) res.redirect('/login#signup') }) } }) .catch((err) => { debug(`Failed to check if somebody already has the email ${req.body.email}`) mw.throwErr(err, req) res.redirect('/signup') }) }) // Forgot password app.route('/login/forgot') // Check if user is already logged in .all((req, res, next) => { if (req.isAuthenticated()) { loginCallback(req, res) } else { next() } }) // Show forgot password page .get((req, res, next) => { res.render('forgot', {email: req.query.email}) }) // Submitted forgot password form .post((req, res, next) => { // Validate email req.checkBody('email', 'Please enter a valid email address.').isEmail() // Check if somebody has that email User.findOne({'email': req.body.email}) .then((user) => { // No user with that email if (!user) { // Don't let on that no such user exists, to prevent dictionary attacks req.flash('success', `If an account exists with the email ${req.body.email}, an email has been sent there with a password reset link. `) res.redirect('/login') // User with that email does exist } else { // Create reset token user.createPassToken((err, token) => { if (err) { next(err) } // Email reset link mail.send({ from: mail.noReply, to: mail.to(user), subject: 'Reset your Tracman password', text: mail.text(`Hi, \n\nDid you request to reset your Tracman password? If so, follow this link to do so:\n${env.url}/settings/password/${token}\n\nIf you didn't initiate this request, just ignore this email. `), html: mail.html(`

Hi,

Did you request to reset your Tracman password? If so, follow this link to do so:
${env.url}/settings/password/${token}

If you didn't initiate this request, just ignore this email.

`) }).then(() => { req.flash('success', `If an account exists with the email ${req.body.email}, an email has been sent there with a password reset link. `) res.redirect('/login') }).catch((err) => { debug(`Failed to send reset link to ${user.email}`) mw.throwErr(err, req) res.redirect('/login') }) }) } }).catch((err) => { debug(`Failed to check for if somebody has that email (in reset request)!`) mw.throwErr(err, req) res.redirect('/login/forgot') }) }) // Android app.post('/login/app', passport.authenticate('local'), appLoginCallback) // Token-based (android social) app.get(['/login/app/google', '/auth/google/idtoken'], passport.authenticate('google-token'), appLoginCallback) // app.get('/login/app/facebook', passport.authenticate('facebook-token'), appLoginCallback); // app.get('/login/app/twitter', passport.authenticate('twitter-token'), appLoginCallback); // Social app.get('/login/:service', (req, res, next) => { let service = req.params.service let sendParams = (service === 'google') ? {scope: ['https://www.googleapis.com/auth/userinfo.profile']} : null // Social login if (!req.user) { debug(`Attempting to login with ${service} with params: ${JSON.stringify(sendParams)}...`) passport.authenticate(service, sendParams)(req, res, next) // Connect social account } else if (!req.user.auth[service]) { debug(`Attempting to connect ${service} account...`) passport.authorize(service, sendParams)(req, res, next) // Disconnect social account } else { debug(`Attempting to disconnect ${service} account...`) // Make sure the user has a password before they disconnect their google login account // This is because login used to only be through google, and some people might not have // set passwords yet... if (!req.user.auth.password && service === 'google') { req.flash('warning', `Hey, you need to set a password before you can disconnect your google account. Otherwise, you won't be able to log in! `) res.redirect('/settings') } else { req.user.auth[service] = undefined req.user.save() .then(() => { req.flash('success', `${mw.capitalize(service)} account disconnected. `) res.redirect('/settings') }) .catch((err) => { debug(`Failed to save user after disconnecting ${service} account!`) mw.throwErr(err, req) res.redirect('/settings') }) } } }) app.get('/login/google/cb', passport.authenticate('google', loginOutcome), loginCallback) app.get('/login/facebook/cb', passport.authenticate('facebook', loginOutcome), loginCallback) app.get('/login/twitter/cb', passport.authenticate('twitter', loginOutcome), loginCallback) }