tracman-server/config/routes/auth.js

396 lines
13 KiB
JavaScript

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
app.route('/login')
.get( (req,res) => {
debug(`GET /login... checking if already logged in`)
// Already logged in
if (req.isAuthenticated()) {
debug(`User is already logged in as ${req.user.id}`)
loginCallback(req,res)
// Show login page
} else {
debug(`Not logged in yet; showing login page`)
res.render('login')
}
})
.post( passport.authenticate('local',loginOutcome), loginCallback );
// Logout
app.get('/logout', (req, res) => {
debug(`Logging out ${req.user.id}`)
req.logout()
req.flash('success',`You have been logged out.`)
res.redirect( req.session.next || '/' )
})
// Signup
app.route('/signup')
.get( (req, res) => {
debug('GET /signup ; redirecting to /login#signup')
res.redirect('/login#signup')
})
.post( (req, res, next) => {
debug('POST /signup')
// 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";
debug(`expirationTimeString set to ${expirationTimeString}`)
// 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(`<p>Welcome to Tracman! </p><p>To complete your registration, follow this link and set your password:<br><a href="${env.url}/settings/password/${token}">${env.url}/settings/password/${token}</a></p><p>This link will expire at ${expirationTimeString}. </p>`)
})
.then(()=>{
debug(`Successfully emailed new user ${user.id} instructions to continue`);
req.flash('success', `An email has been sent to <u>${user.email}</u>. Check your inbox and follow the link to complete your registration. (Your registration link will expire in one hour). `);
res.redirect('/login');
})
.then( () => {
debug(`Successfully emailed new user ${user.id} instructions to continue`)
req.flash('success', `An email has been sent to <u>${user.email}</u>. 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 <a href="/login/forgot?email=${req.body.email}">reset it here</a>.`)
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... resending token`)
// 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()
})
// Slug created successfully
}) (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( user.save )
.then( () => {
debug(`User saved; sending token...`)
sendToken(user)
user.save()
})
.catch( (err)=>{
debug('Failed to save user after creating slug and sk32! Token not sent')
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('/login#signup')
})
})
// Forgot password
app.route('/login/forgot')
.all( (req, res, next) => {
debug(`/login/fogot requested`)
// Check if user is already logged in
if (req.isAuthenticated()) {
debug(`User is already logged in as ${req.user.id}`)
loginCallback(req,res)
} else {
next()
}
} )
// Show forgot password page
.get( (req, res, next) => {
debug(`GET /login/forgot ; loading forgot password form...`)
res.render('forgot', {email:req.query.email})
} )
// Submitted forgot password form
.post( (req, res, next) => {
debug(`POST /login/forgot ; checking email ${req.body.email}...`)
// 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) {
debug(`No user with that email exists; Ignoring request to reset password`)
// Don't let on that no such user exists, to prevent dictionary attacks
req.flash('success', `If an account exists with the email <u>${req.body.email}</u>, an email has been sent there with a password reset link. `)
res.redirect('/login')
// User with that email does exist
} else {
debug(`User ${user.id} has that email; creating reset token...`)
// Create reset token
user.createPassToken( (err, token) => {
if (err) {
debug('Error creating password token')
next(err)
}
// Token created successfully
if (token) {
debug(`Token ${token} created; sending email to ${user.email}...`)
// 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(`<p>Hi, </p><p>Did you request to reset your Tracman password? If so, follow this link to do so:<br><a href="${env.url}/settings/password/${token}">${env.url}/settings/password/${token}</a></p><p>If you didn't initiate this request, just ignore this email. </p>`)
})
// Successfully sent reset link
.then( () => {
debug(`Reset link sent successfully`)
req.flash('success', `If an account exists with the email <u>${req.body.email}</u>, an email has been sent there with a password reset link. `)
res.redirect('/login')
})
// Failed to email reset link
.catch( (err) => {
debug(`Failed to send reset link`)
mw.throwErr(err, req)
res.redirect('/login')
})
}
})
}
})
// Failed to check if somebody has that email
.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) => {
debug(`GET /login/:service for ${service} service`)
// Set params
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') {
debug(`User doesn't have a password yet and can't disconnect their google account`)
req.flash('warning',`Hey, you need to <a href="/settings/password">set a password</a> before you can disconnect your google account. Otherwise, you won't be able to log in! `)
res.redirect('/settings')
} else {
debug(`Disconnecting ${service} account`)
req.user.auth[service] = undefined
req.user.save()
.then( () => {
debug(`${mw.capitalize(service)} account disconnected successfully`)
req.flash('success',`${mw.capitalize(service)} account disconnected`)
res.redirect('/settings')
})
.catch( (err) => {
debug(`Unable to disconnect ${service} account`)
mw.throwErr(err,req)
res.redirect('/settings')
})
}
}
})
// Login callbacks
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 )
}