From 016378d8077d64ff4db2955884679dd6e69ae953 Mon Sep 17 00:00:00 2001 From: Keith Irwin Date: Wed, 13 Dec 2017 00:40:07 +0000 Subject: [PATCH] #110 Implemented StandardJS --- CHANGELOG.md | 2 + README.md | 4 + config/demo.js | 66 ++- config/env/sample.js | 88 ++-- config/mail.js | 68 +-- config/middleware.js | 76 ++-- config/models.js | 276 ++++++------ config/passport.js | 475 ++++++++++----------- config/routes/admin.js | 68 ++- config/routes/auth.js | 646 +++++++++++++--------------- config/routes/contact.js | 156 ++++--- config/routes/index.js | 194 ++++----- config/routes/map.js | 139 +++--- config/routes/settings.js | 622 +++++++++++++-------------- config/routes/test.js | 91 ++-- config/sockets.js | 220 +++++----- package.json | 2 +- server.js | 364 ++++++++-------- static/js/base.js | 24 +- static/js/contact.js | 126 +++--- static/js/footer.js | 18 +- static/js/header.js | 52 ++- static/js/html5shiv.js | 34 +- static/js/login.js | 24 +- static/js/map.js | 869 ++++++++++++++++++-------------------- static/js/password.js | 149 +++---- static/js/settings.js | 275 ++++++------ test.js | 291 +++++++------ webpack.config.js | 64 +-- 29 files changed, 2631 insertions(+), 2852 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd76eb8..d514c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Tracman Server Changelog ###### v 0.7.12 +#### develop +* [#110](https://github.com/Tracman-org/Server/issues/110) Implemented [StandardJS](https://standardjs.com/) #### v0.7.12 * Fixed altitude sign diff --git a/README.md b/README.md index 3add2ef..b3c2de2 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ node.js application to display a sharable map with user's location. +[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) ## Installation @@ -52,6 +53,9 @@ Tracman will be updated according to [this branching model](http://nvie.com/post [view full changelog](CHANGELOG.md) +#### develop +* [#110](https://github.com/Tracman-org/Server/issues/110) Implemented [StandardJS](https://standardjs.com/) + #### v0.7.12 * Fixed altitude sign diff --git a/config/demo.js b/config/demo.js index 5404759..0f9317f 100644 --- a/config/demo.js +++ b/config/demo.js @@ -1,38 +1,34 @@ -'use strict'; +'use strict' // Imports -const fs = require('fs'), - debug = require('debug')('tracman-demo'); +const fs = require('fs') +const path = require('path') +const debug = require('debug')('tracman-demo') -module.exports = (io)=>{ - - // File is space-seperated: delay, lat, lon, dir, spd - fs.readFile(__dirname+'/demo.txt', (err,data)=>{ - if (err){ console.error(`❌ ${err.stack}`); } - - const lines = data.toString().split('\n'); - - (function sendLoc(ln) { - if (ln>20754){ sendLoc(0) } - else { - - let loc = lines[ln].split(' '); - debug(`Sending demo location: ${loc[1]}, ${loc[2]}`); - io.to('demo').emit('get', { - tim: new Date(), - lat: loc[1], - lon: loc[2], - dir: loc[3], - spd: loc[4] - }); - - // Repeat after delay in milliseconds - setTimeout(()=>{ - sendLoc(ln+1); // next line of file - }, loc[0]); - - } - })(5667); - - }); -}; \ No newline at end of file +module.exports = (io) => { + // File is space-seperated: delay, lat, lon, dir, spd + fs.readFile(path.join(__dirname, '/demo.txt'), (err, data) => { + if (err) { console.error(`❌ ${err.stack}`) } + + const lines = data.toString().split('\n'); + + (function sendLoc (ln) { + if (ln > 20754) { sendLoc(0) } else { + let loc = lines[ln].split(' ') + debug(`Sending demo location: ${loc[1]}, ${loc[2]}`) + io.to('demo').emit('get', { + tim: new Date(), + lat: loc[1], + lon: loc[2], + dir: loc[3], + spd: loc[4] + }) + + // Repeat after delay in milliseconds + setTimeout(() => { + sendLoc(ln + 1) // next line of file + }, loc[0]) + } + })(5667) + }) +} diff --git a/config/env/sample.js b/config/env/sample.js index afdee14..df97fbc 100644 --- a/config/env/sample.js +++ b/config/env/sample.js @@ -1,46 +1,46 @@ -'use strict'; +'use strict' module.exports = { - - // Local variables - mode: 'development', // or production - - // Random strings to prevent hijacking - session: 'SomeSecret', - cookie: 'SomeOtherSecret', - - // Location of your mongoDB - mongoSetup: 'mongodb://localhost:27017/tracman', - // Or use the test database from mLab - //mongoSetup: 'mongodb://tracman:MUPSLXQ34f9cQTc5@ds133961.mlab.com:33961/tracman', - // You can log in there with: - // mongo ds133961.mlab.com:33961/tracman-dev -u contributor -p opensourcerules - - // URL and port where this will run - url: 'https://localhost:8080', - port: 8080, - - // Mailserver - mailserver: 'example.org', - mailport: 587, - mailauth: { - user: 'mailusername', - pass: 'XXXXXXXXXX', - }, - - // OAuth API keys - facebookAppId: 'XXXXXXXXXXXXXXXX', - facebookAppSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - twitterConsumerKey: 'XXXXXXXXXXXXXXXXXXXXXXXXX', - twitterConsumerSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - googleClientId: '############-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com', - googleClientSecret: 'XXXXXXXXX_XXXXXXXXXXXXXX', - - // Google maps API key - googleMapsAPI: 'XXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXX', - - // reCaptcha API key - recaptchaSitekey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - recaptchaSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - -}; + + // Local variables + mode: 'development', // or production + + // Random strings to prevent hijacking + session: 'SomeSecret', + cookie: 'SomeOtherSecret', + + // Location of your mongoDB + mongoSetup: 'mongodb://localhost:27017/tracman', + // Or use the test database from mLab + // mongoSetup: 'mongodb://tracman:MUPSLXQ34f9cQTc5@ds133961.mlab.com:33961/tracman', + // You can log in there with: + // mongo ds133961.mlab.com:33961/tracman-dev -u contributor -p opensourcerules + + // URL and port where this will run + url: 'https://localhost:8080', + port: 8080, + + // Mailserver + mailserver: 'example.org', + mailport: 587, + mailauth: { + user: 'mailusername', + pass: 'XXXXXXXXXX' + }, + + // OAuth API keys + facebookAppId: 'XXXXXXXXXXXXXXXX', + facebookAppSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + twitterConsumerKey: 'XXXXXXXXXXXXXXXXXXXXXXXXX', + twitterConsumerSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + googleClientId: '############-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com', + googleClientSecret: 'XXXXXXXXX_XXXXXXXXXXXXXX', + + // Google maps API key + googleMapsAPI: 'XXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXX', + + // reCaptcha API key + recaptchaSitekey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + recaptchaSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' + +} diff --git a/config/mail.js b/config/mail.js index 6d9acfe..5818897 100644 --- a/config/mail.js +++ b/config/mail.js @@ -1,41 +1,41 @@ -'use strict'; +'use strict' -const nodemailer = require('nodemailer'), - env = require('./env/env.js'); +const nodemailer = require('nodemailer') +const env = require('./env/env.js') let transporter = nodemailer.createTransport({ - host: env.mailserver, - port: env.mailport, - secure: false, - requireTLS: true, - auth: env.mailauth, + host: env.mailserver, + port: env.mailport, + secure: false, + requireTLS: true, + auth: env.mailauth // logger: true, // debug: true -}); +}) module.exports = { - - verify: ()=>{ - transporter.verify( (err,success)=>{ - if (err){ console.error(`SMTP Error: ${err}`); } - console.log(`📧 SMTP ${!success?'not ':''}ready`); - } ); - }, - - send: transporter.sendMail.bind(transporter), - - text: (text)=>{ - return `Tracman\n\n${text}\n\nDo not reply to this email\nFor information about why you received this email, see the privacy policy at ${env.url}/privacyy#email`; - }, - - html: (text)=>{ - return `

+Tracman

${text}

Do not reply to this email. For information about why you recieved this email, see our privacy policy.

`; - }, - - noReply: `"Tracman" `, - - to: (user)=>{ - return `"${user.name}" <${user.email}>`; - } - -}; \ No newline at end of file + + verify: () => { + transporter.verify((err, success) => { + if (err) { console.error(`SMTP Error: ${err}`) } + console.log(`📧 SMTP ${!success ? 'not ' : ''}ready`) + }) + }, + + send: transporter.sendMail.bind(transporter), + + text: (text) => { + return `Tracman\n\n${text}\n\nDo not reply to this email\nFor information about why you received this email, see the privacy policy at ${env.url}/privacyy#email` + }, + + html: (text) => { + return `

+Tracman

${text}

Do not reply to this email. For information about why you recieved this email, see our privacy policy.

` + }, + + noReply: `"Tracman" `, + + to: (user) => { + return `"${user.name}" <${user.email}>` + } + +} diff --git a/config/middleware.js b/config/middleware.js index 3d9853d..2c2d5ba 100644 --- a/config/middleware.js +++ b/config/middleware.js @@ -1,47 +1,45 @@ -'use strict'; +'use strict' -const env = require('./env/env.js'); +const env = require('./env/env.js') module.exports = { - // Throw error - throwErr: (err,req=null)=>{ - console.error(`❌️ ${err.stack}`); - if (req){ - if (env.mode==='production') { - req.flash('danger', 'An error occured.
Would you like to report it?'); - } else { // development - req.flash('danger', err.message); - } - } - }, + // Throw error + throwErr: (err, req = null) => { + console.error(`❌️ ${err.stack}`) + if (req) { + if (env.mode === 'production') { + req.flash('danger', 'An error occured.
Would you like to report it?') + } else { // development + req.flash('danger', err.message) + } + } + }, - // Capitalize the first letter of a string - capitalize: (str)=>{ - return str.charAt(0).toUpperCase() + str.slice(1); - }, - - // Validate an email address - validateEmail: (email)=>{ - var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - return re.test(email); - }, + // Capitalize the first letter of a string + capitalize: (str) => { + return str.charAt(0).toUpperCase() + str.slice(1) + }, - // Ensure authentication - ensureAuth: (req,res,next)=>{ - if (req.isAuthenticated()) { return next(); } - else { res.redirect('/login'); } - }, + // Validate an email address + validateEmail: (email) => { + var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(email) + }, - // Ensure administrator - ensureAdmin: (req,res,next)=>{ - if (req.isAuthenticated() && req.user.isAdmin){ return next(); } - else { - let err = new Error("Unauthorized"); - err.status = 401; - next(err); - } - //TODO: test this by logging in as !isAdmin and go to /admin - } + // Ensure authentication + ensureAuth: (req, res, next) => { + if (req.isAuthenticated()) { return next() } else { res.redirect('/login') } + }, -}; \ No newline at end of file + // Ensure administrator + ensureAdmin: (req, res, next) => { + if (req.isAuthenticated() && req.user.isAdmin) { return next() } else { + let err = new Error('Unauthorized') + err.status = 401 + next(err) + } + // TODO: test this by logging in as !isAdmin and go to /admin + } + +} diff --git a/config/models.js b/config/models.js index 833c678..373ab4b 100644 --- a/config/models.js +++ b/config/models.js @@ -1,150 +1,142 @@ -'use strict'; +'use strict' -const mongoose = require('mongoose'), - unique = require('mongoose-unique-validator'), - bcrypt = require('bcrypt'), - crypto = require('crypto'), - debug = require('debug')('tracman-models'); +const mongoose = require('mongoose') +const unique = require('mongoose-unique-validator') +const bcrypt = require('bcrypt') +const crypto = require('crypto') +const debug = require('debug')('tracman-models') const userSchema = new mongoose.Schema({ - name: {type:String}, - email: {type:String, unique:true}, - newEmail: String, - emailToken: String, - slug: {type:String, required:true, unique:true}, - auth: { - password: String, - passToken: String, - passTokenExpires: Date, - google: String, - facebook: String, - twitter: String, - }, - isAdmin: {type:Boolean, required:true, default:false}, - isPro: {type:Boolean, required:true, default:false}, - created: {type:Date, required:true}, - lastLogin: Date, - settings: { - units: {type:String, default:'standard'}, - defaultMap: {type:String, default:'road'}, - defaultZoom: {type:Number, default:11}, - showScale: {type:Boolean, default:false}, - showSpeed: {type:Boolean, default:false}, - showTemp: {type:Boolean, default:false}, - showAlt: {type:Boolean, default:false}, - showStreetview: {type:Boolean, default:false}, - marker: {type:String, default:'red'} - }, - last: { - time: Date, - lat: {type:Number, default:0}, - lon: {type:Number, default:0}, - dir: {type:Number, default:0}, - alt: {type:Number}, - spd: {type:Number, default:0} - }, - sk32: {type:String, required:true, unique:true} -}).plugin(unique); + name: {type: String}, + email: {type: String, unique: true}, + newEmail: String, + emailToken: String, + slug: {type: String, required: true, unique: true}, + auth: { + password: String, + passToken: String, + passTokenExpires: Date, + google: String, + facebook: String, + twitter: String + }, + isAdmin: {type: Boolean, required: true, default: false}, + isPro: {type: Boolean, required: true, default: false}, + created: {type: Date, required: true}, + lastLogin: Date, + settings: { + units: {type: String, default: 'standard'}, + defaultMap: {type: String, default: 'road'}, + defaultZoom: {type: Number, default: 11}, + showScale: {type: Boolean, default: false}, + showSpeed: {type: Boolean, default: false}, + showTemp: {type: Boolean, default: false}, + showAlt: {type: Boolean, default: false}, + showStreetview: {type: Boolean, default: false}, + marker: {type: String, default: 'red'} + }, + last: { + time: Date, + lat: {type: Number, default: 0}, + lon: {type: Number, default: 0}, + dir: {type: Number, default: 0}, + alt: {type: Number}, + spd: {type: Number, default: 0} + }, + sk32: {type: String, required: true, unique: true} +}).plugin(unique) -/* User methods */ { - - //TODO: Return promises instead of taking callbacks - // See https://gist.github.com/7h1b0/5154fda207e68ad1cefc#file-random-js - // For an example - - // Create email confirmation token - userSchema.methods.createEmailToken = function(next){ // next(err,token) - debug('user.createEmailToken() called'); - var user = this; - - crypto.randomBytes(16, (err,buf)=>{ - if (err){ next(err,null); } - if (buf){ - debug(`Buffer ${buf.toString('hex')} created`); - user.emailToken = buf.toString('hex'); - user.save() - .then( ()=>{ - return next(null,user.emailToken); - }) - .catch( (err)=>{ - return next(err,null); - }); - - } - }); - - }; +/* User methods */ +// TODO: Return promises instead of taking callbacks +// See https://gist.github.com/7h1b0/5154fda207e68ad1cefc#file-random-js +// For an example - // Create password reset token - userSchema.methods.createPassToken = function(next){ // next(err,token,expires) - var user = this; - - // Reuse old token, resetting clock - if ( user.auth.passTokenExpires >= Date.now() ){ - debug(`Reusing old password token...`); - user.auth.passTokenExpires = Date.now() + 3600000; // 1 hour - user.save() - .then( ()=>{ - return next(null,user.auth.passToken,user.auth.passTokenExpires); - }) - .catch( (err)=>{ - return next(err,null,null); - }); - } - - // Create new token - else { - debug(`Creating new password token...`); - crypto.randomBytes(16, (err,buf)=>{ - if (err){ return next(err,null,null); } - if (buf) { - user.auth.passToken = buf.toString('hex'); - user.auth.passTokenExpires = Date.now() + 3600000; // 1 hour - user.save() - .then( ()=>{ - debug('successfully saved user in createPassToken'); - return next(null,user.auth.passToken,user.auth.passTokenExpires); - }) - .catch( (err)=>{ - debug('error saving user in createPassToken'); - return next(err,null,null); - }); - } - }); - } - - }; - - // Generate hash for new password and save it to the database - userSchema.methods.generateHashedPassword = function(password,next){ - // next(err); - - // Delete token - this.auth.passToken = undefined; - this.auth.passTokenExpires = undefined; - - // Generate hash - bcrypt.genSalt(8, (err,salt)=>{ - if (err){ return next(err); } - bcrypt.hash(password, salt, (err,hash)=>{ - if (err){ return next(err); } - this.auth.password = hash; - this.save(); - next(); - }); - }); - - }; - - // Check for valid password - userSchema.methods.validPassword = function(password,next){ - // next(err,res); - // res = true/false - bcrypt.compare(password, this.auth.password, next); - }; - +// Create email confirmation token +userSchema.methods.createEmailToken = function (next) { // next(err,token) + debug('user.createEmailToken() called') + var user = this + + crypto.randomBytes(16, (err, buf) => { + if (err) { next(err, null) } + if (buf) { + debug(`Buffer ${buf.toString('hex')} created`) + user.emailToken = buf.toString('hex') + user.save() + .then(() => { + return next(null, user.emailToken) + }) + .catch((err) => { + return next(err, null) + }) + } + }) +} + +// Create password reset token +userSchema.methods.createPassToken = function (next) { // next(err,token,expires) + var user = this + + // Reuse old token, resetting clock + if (user.auth.passTokenExpires >= Date.now()) { + debug(`Reusing old password token...`) + user.auth.passTokenExpires = Date.now() + 3600000 // 1 hour + user.save() + .then(() => { + return next(null, user.auth.passToken, user.auth.passTokenExpires) + }) + .catch((err) => { + return next(err, null, null) + }) + + // Create new token + } else { + debug(`Creating new password token...`) + crypto.randomBytes(16, (err, buf) => { + if (err) { return next(err, null, null) } + if (buf) { + user.auth.passToken = buf.toString('hex') + user.auth.passTokenExpires = Date.now() + 3600000 // 1 hour + user.save() + .then(() => { + debug('successfully saved user in createPassToken') + return next(null, user.auth.passToken, user.auth.passTokenExpires) + }) + .catch((err) => { + debug('error saving user in createPassToken') + return next(err, null, null) + }) + } + }) + } +} + +// Generate hash for new password and save it to the database +userSchema.methods.generateHashedPassword = function (password, next) { + // next(err); + + // Delete token + this.auth.passToken = undefined + this.auth.passTokenExpires = undefined + + // Generate hash + bcrypt.genSalt(8, (err, salt) => { + if (err) { return next(err) } + bcrypt.hash(password, salt, (err, hash) => { + if (err) { return next(err) } + this.auth.password = hash + this.save() + next() + }) + }) +} + +// Check for valid password +userSchema.methods.validPassword = function (password, next) { + // next(err,res); + // res = true/false + bcrypt.compare(password, this.auth.password, next) } module.exports = { - 'user': mongoose.model('User', userSchema) -}; + 'user': mongoose.model('User', userSchema) +} diff --git a/config/passport.js b/config/passport.js index 5b19619..c01e34b 100644 --- a/config/passport.js +++ b/config/passport.js @@ -1,252 +1,229 @@ -'use strict'; +'use strict' -const - LocalStrategy = require('passport-local').Strategy, - GoogleStrategy = require('passport-google-oauth20').Strategy, - FacebookStrategy = require('passport-facebook').Strategy, - TwitterStrategy = require('passport-twitter').Strategy, - GoogleTokenStrategy = require('passport-google-id-token'), - FacebookTokenStrategy = require('passport-facebook-token'), - TwitterTokenStrategy = require('passport-twitter-token'), - debug = require('debug')('tracman-passport'), - env = require('./env/env.js'), - mw = require('./middleware.js'), - User = require('./models.js').user; - -module.exports = (passport)=>{ - - // Serialize/deserialize users - passport.serializeUser((user,done)=>{ - done(null, user.id); - }); - passport.deserializeUser((id,done)=>{ - User.findById(id, (err,user)=>{ - if(!err){ done(null, user); } - else { done(err, null); } - }); - }); - - // Local - passport.use('local', new LocalStrategy({ - usernameField: 'email', - passwordField: 'password', - passReqToCallback: true - }, (req,email,password,done)=>{ - debug(`Perfoming local login for ${email}`); - User.findOne({'email':email}) - .then( (user)=>{ - - // No user with that email - if (!user) { - req.session.next = undefined; - return done( null, false, req.flash('warning','Incorrect email or password.') ); - } - - // User exists - else { - - // Check password - user.validPassword( password, (err,res)=>{ - if (err){ return done(err); } - - // Password incorrect - if (!res) { - req.session.next = undefined; - return done( null, false, req.flash('warning','Incorrect email or password.') ); - } - - // Successful login - else { - user.lastLogin = Date.now(); - user.save(); - return done(null,user); - } - - } ); - } - - }) - .catch( (err)=>{ - return done(err); - }); - } - )); - - // Social login - function socialLogin(req, service, profileId, done) { - debug(`socialLogin() called for ${service} account ${profileId}`); - let query = {}; - query['auth.'+service] = profileId; - - // Intent to log in - if (!req.user) { - debug(`Searching for user with query ${query}...`); - User.findOne(query) - .then( (user)=>{ - - // Can't find user - if (!user){ - - // Lazy update from old googleId field - if (service==='google') { - User.findOne({ 'googleID': parseInt(profileId,10) }) - .then( (user)=>{ - - // User exists with old schema - if (user) { - debug(`User ${user.id} exists with old schema. Lazily updating...`); - user.auth.google = profileId; - user.googleId = undefined; - user.save() - .then( ()=>{ - debug(`Lazily updated ${user.id}...`); - req.session.flashType = 'success'; - req.session.flashMessage = "You have been logged in. "; - return done(null, user); - }) - .catch( (err)=>{ - debug(`Failed to save user that exists with old googleId schema!`); - mw.throwErr(err,req); - return done(err); - }); - } - - // No such user - else { - debug(`User with ${service} account of ${profileId} not found.`); - req.flash('warning', `There's no user for that ${service} account. `); - return done(); - } - - }) - .catch ( (err)=>{ - debug(`Failed to search for user with old googleID of ${profileId}. `); - mw.throwErr(err,req); - return done(err); - }); - } - - // No googleId either - else { - debug(`Couldn't find ${service} user with profileID ${profileId}.`); - req.flash('warning', `There's no user for that ${service} account. `); - return done(); - } - } - - // Successfull social login - else { - debug(`Found user: ${user.id}; logging in...`); - req.session.flashType = 'success'; - req.session.flashMessage = "You have been logged in."; - return done(null, user); - } - - }) - .catch( (err)=>{ - debug(`Failed to find user with query: ${query}`); - mw.throwErr(err,req); - return done(err); - }); - } - - // Intent to connect account - else { - debug(`Attempting to connect ${service} account to ${req.user.id}...`); - - // Check for unique profileId - debug(`Checking for unique account with query ${query}...`); - User.findOne(query) - .then( (existingUser)=>{ - - // Social account already in use - if (existingUser) { - debug(`${service} account already in use with user ${existingUser.id}`); - req.session.flashType = 'warning'; - req.session.flashMessage = `Another user is already connected to that ${service} account. `; - return done(); - } - - // Connect to account - else { - debug(`${service} account (${profileId}) is unique; Connecting to ${req.user.id}...`); - req.user.auth[service] = profileId; - req.user.save() - .then( ()=>{ - debug(`Successfully connected ${service} account to ${req.user.id}`); - req.session.flashType = 'success'; - req.session.flashMessage = `${mw.capitalize(service)} account connected. `; - return done(null,req.user); - } ) - .catch( (err)=>{ - debug(`Failed to connect ${service} account to ${req.user.id}!`); - return done(err); - } ); - } - - }) - .catch( (err)=>{ - debug(`Failed to check for unique ${service} profileId of ${profileId}!`); - mw.throwErr(err,req); - return done(err); - }); - - } - - } +const LocalStrategy = require('passport-local').Strategy +const GoogleStrategy = require('passport-google-oauth20').Strategy +const FacebookStrategy = require('passport-facebook').Strategy +const TwitterStrategy = require('passport-twitter').Strategy +const GoogleTokenStrategy = require('passport-google-id-token') +const FacebookTokenStrategy = require('passport-facebook-token') +const TwitterTokenStrategy = require('passport-twitter-token') +const debug = require('debug')('tracman-passport') +const env = require('./env/env.js') +const mw = require('./middleware.js') +const User = require('./models.js').user - // Google - passport.use('google', new GoogleStrategy({ - clientID: env.googleClientId, - clientSecret: env.googleClientSecret, - callbackURL: env.url+'/login/google/cb', - passReqToCallback: true - }, (req, accessToken, refreshToken, profile, done)=>{ - socialLogin(req, 'google', profile.id, done); - } - )).use('google-token', new GoogleTokenStrategy({ - clientID: env.googleClientId, - passReqToCallback: true - }, (req, parsedToken, googleId, done)=>{ - socialLogin(req,'google', googleId, done); - } - )); - - // Facebook - passport.use('facebook', new FacebookStrategy({ - clientID: env.facebookAppId, - clientSecret: env.facebookAppSecret, - callbackURL: env.url+'/login/facebook/cb', - passReqToCallback: true - }, (req, accessToken, refreshToken, profile, done)=>{ - socialLogin(req, 'facebook', profile.id, done); - } - )).use('facebook-token', new FacebookTokenStrategy({ - clientID: env.facebookAppId, - clientSecret: env.facebookAppSecret, - passReqToCallback: true - }, (req, accessToken, refreshToken, profile, done)=>{ - socialLogin(req,'facebook', profile.id, done); - } - )); - - // Twitter - passport.use(new TwitterStrategy({ - consumerKey: env.twitterConsumerKey, - consumerSecret: env.twitterConsumerSecret, - callbackURL: env.url+'/login/twitter/cb', - passReqToCallback: true - }, (req, token, tokenSecret, profile, done)=>{ - socialLogin(req, 'twitter', profile.id, done); - } - )).use('twitter-token', new TwitterTokenStrategy({ - consumerKey: env.twitterConsumerKey, - consumerSecret: env.twitterConsumerSecret, - passReqToCallback: true - }, (req, token, tokenSecret, profile, done)=>{ - socialLogin(req,'twitter', profile.id, done); - } - )); +module.exports = (passport) => { + // Serialize/deserialize users + passport.serializeUser((user, done) => { + done(null, user.id) + }) + passport.deserializeUser((id, done) => { + User.findById(id, (err, user) => { + if (!err) { done(null, user) } else { done(err, null) } + }) + }) - return passport; -}; \ No newline at end of file + // Local + passport.use('local', new LocalStrategy({ + usernameField: 'email', + passwordField: 'password', + passReqToCallback: true + }, (req, email, password, done) => { + debug(`Perfoming local login for ${email}`) + User.findOne({'email': email}) + .then((user) => { + // No user with that email + if (!user) { + req.session.next = undefined + return done(null, false, req.flash('warning', 'Incorrect email or password.')) + + // User exists + } else { + // Check password + user.validPassword(password, (err, res) => { + if (err) { return done(err) } + + // Password incorrect + if (!res) { + req.session.next = undefined + return done(null, false, req.flash('warning', 'Incorrect email or password.')) + + // Successful login + } else { + user.lastLogin = Date.now() + user.save() + return done(null, user) + } + }) + } + }) + .catch((err) => { + return done(err) + }) + } + )) + + // Social login + function socialLogin (req, service, profileId, done) { + debug(`socialLogin() called for ${service} account ${profileId}`) + let query = {} + query['auth.' + service] = profileId + + // Intent to log in + if (!req.user) { + debug(`Searching for user with query ${query}...`) + User.findOne(query) + .then((user) => { + // Can't find user + if (!user) { + // Lazy update from old googleId field + if (service === 'google') { + User.findOne({ 'googleID': parseInt(profileId, 10) }) + .then((user) => { + // User exists with old schema + if (user) { + debug(`User ${user.id} exists with old schema. Lazily updating...`) + user.auth.google = profileId + user.googleId = undefined + user.save() + .then(() => { + debug(`Lazily updated ${user.id}...`) + req.session.flashType = 'success' + req.session.flashMessage = 'You have been logged in. ' + return done(null, user) + }) + .catch((err) => { + debug(`Failed to save user that exists with old googleId schema!`) + mw.throwErr(err, req) + return done(err) + }) + + // No such user + } else { + debug(`User with ${service} account of ${profileId} not found.`) + req.flash('warning', `There's no user for that ${service} account. `) + return done() + } + }) + .catch((err) => { + debug(`Failed to search for user with old googleID of ${profileId}. `) + mw.throwErr(err, req) + return done(err) + }) + + // No googleId either + } else { + debug(`Couldn't find ${service} user with profileID ${profileId}.`) + req.flash('warning', `There's no user for that ${service} account. `) + return done() + } + + // Successfull social login + } else { + debug(`Found user: ${user.id}; logging in...`) + req.session.flashType = 'success' + req.session.flashMessage = 'You have been logged in.' + return done(null, user) + } + }) + .catch((err) => { + debug(`Failed to find user with query: ${query}`) + mw.throwErr(err, req) + return done(err) + }) + + // Intent to connect account + } else { + debug(`Attempting to connect ${service} account to ${req.user.id}...`) + + // Check for unique profileId + debug(`Checking for unique account with query ${query}...`) + User.findOne(query) + .then((existingUser) => { + // Social account already in use + if (existingUser) { + debug(`${service} account already in use with user ${existingUser.id}`) + req.session.flashType = 'warning' + req.session.flashMessage = `Another user is already connected to that ${service} account. ` + return done() + + // Connect to account + } else { + debug(`${service} account (${profileId}) is unique; Connecting to ${req.user.id}...`) + req.user.auth[service] = profileId + req.user.save() + .then(() => { + debug(`Successfully connected ${service} account to ${req.user.id}`) + req.session.flashType = 'success' + req.session.flashMessage = `${mw.capitalize(service)} account connected. ` + return done(null, req.user) + }) + .catch((err) => { + debug(`Failed to connect ${service} account to ${req.user.id}!`) + return done(err) + }) + } + }) + .catch((err) => { + debug(`Failed to check for unique ${service} profileId of ${profileId}!`) + mw.throwErr(err, req) + return done(err) + }) + } + } + + // Google + passport.use('google', new GoogleStrategy({ + clientID: env.googleClientId, + clientSecret: env.googleClientSecret, + callbackURL: env.url + '/login/google/cb', + passReqToCallback: true + }, (req, accessToken, refreshToken, profile, done) => { + socialLogin(req, 'google', profile.id, done) + } + )).use('google-token', new GoogleTokenStrategy({ + clientID: env.googleClientId, + passReqToCallback: true + }, (req, parsedToken, googleId, done) => { + socialLogin(req, 'google', googleId, done) + } + )) + + // Facebook + passport.use('facebook', new FacebookStrategy({ + clientID: env.facebookAppId, + clientSecret: env.facebookAppSecret, + callbackURL: env.url + '/login/facebook/cb', + passReqToCallback: true + }, (req, accessToken, refreshToken, profile, done) => { + socialLogin(req, 'facebook', profile.id, done) + } + )).use('facebook-token', new FacebookTokenStrategy({ + clientID: env.facebookAppId, + clientSecret: env.facebookAppSecret, + passReqToCallback: true + }, (req, accessToken, refreshToken, profile, done) => { + socialLogin(req, 'facebook', profile.id, done) + } + )) + + // Twitter + passport.use(new TwitterStrategy({ + consumerKey: env.twitterConsumerKey, + consumerSecret: env.twitterConsumerSecret, + callbackURL: env.url + '/login/twitter/cb', + passReqToCallback: true + }, (req, token, tokenSecret, profile, done) => { + socialLogin(req, 'twitter', profile.id, done) + } + )).use('twitter-token', new TwitterTokenStrategy({ + consumerKey: env.twitterConsumerKey, + consumerSecret: env.twitterConsumerSecret, + passReqToCallback: true + }, (req, token, tokenSecret, profile, done) => { + socialLogin(req, 'twitter', profile.id, done) + } + )) + + return passport +} diff --git a/config/routes/admin.js b/config/routes/admin.js index 3622e11..4cd3b0a 100644 --- a/config/routes/admin.js +++ b/config/routes/admin.js @@ -1,39 +1,35 @@ -'use strict'; +'use strict' -const router = require('express').Router(), - mw = require('../middleware.js'), - debug = require('debug')('tracman-routes-admin'), - User = require('../models.js').user; +const router = require('express').Router() +const mw = require('../middleware.js') +const debug = require('debug')('tracman-routes-admin') +const User = require('../models.js').user -router.get('/', mw.ensureAdmin, (req,res)=>{ - - User.find({}).sort({lastLogin:-1}) - .then( (found)=>{ - res.render('admin', { - active: 'admin', - noFooter: '1', - users: found, - total: found.length - }); - }) - .catch( (err)=>{ mw.throwErr(err,req); }); - -}); - -router.get('/delete/:usrid', mw.ensureAdmin, (req,res,next)=>{ - - debug(`/delete/${req.params.usrid} called`); - - User.findOneAndRemove({'_id':req.params.usrid}) - .then( (user)=>{ - req.flash('success', `${req.params.usrid} deleted.`); - res.redirect('/admin'); - }) - .catch( (err)=>{ - mw.throwErr(err,req); - res.redirect('/admin'); - }); - -}); +router.get('/', mw.ensureAdmin, (req, res) => { + User.find({}).sort({lastLogin: -1}) + .then((found) => { + res.render('admin', { + active: 'admin', + noFooter: '1', + users: found, + total: found.length + }) + }) + .catch((err) => { mw.throwErr(err, req) }) +}) -module.exports = router; +router.get('/delete/:usrid', mw.ensureAdmin, (req, res, next) => { + debug(`/delete/${req.params.usrid} called`) + + User.findOneAndRemove({'_id': req.params.usrid}) + .then((user) => { + req.flash('success', `${req.params.usrid} deleted.`) + res.redirect('/admin') + }) + .catch((err) => { + mw.throwErr(err, req) + res.redirect('/admin') + }) +}) + +module.exports = router diff --git a/config/routes/auth.js b/config/routes/auth.js index 31eb927..b2129fd 100644 --- a/config/routes/auth.js +++ b/config/routes/auth.js @@ -1,345 +1,309 @@ -'use strict'; +'use strict' -const - mw = require('../middleware.js'), - mail = require('../mail.js'), - User = require('../models.js').user, - crypto = require('crypto'), - moment = require('moment'), - slugify = require('slug'), - debug = require('debug')('tracman-routes-auth'), - env = require('../env/env.js'); +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) + } + } - // Methods for success and failure - const - loginOutcome = { - failureRedirect: '/login', - failureFlash: true - }, - 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' ); - }, - 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, - 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 ); - -}; + // 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) +} diff --git a/config/routes/contact.js b/config/routes/contact.js index eb20543..aa44faa 100644 --- a/config/routes/contact.js +++ b/config/routes/contact.js @@ -1,92 +1,78 @@ -'use strict'; +'use strict' -const env = require('../env/env.js'), - request = require('request'), - mw = require('../middleware.js'), - mail = require('../mail.js'), - router = require('express').Router(); +const env = require('../env/env.js') +const request = require('request') +const mw = require('../middleware.js') +const mail = require('../mail.js') +const router = require('express').Router() module.exports = router // Display contact form -.get('/', (req,res)=>{ - res.render('contact', {active:'contact', - sitekey: env.recaptchaSitekey - }); +.get('/', (req, res) => { + res.render('contact', {active: 'contact', + sitekey: env.recaptchaSitekey + }) }) -.post('/', (req,res,next)=>{ - - // Check email - if (req.body.email==='') { - req.flash('warning', `You need to enter an email address. `); - res.redirect('/contact'); - } - else if (!mw.validateEmail(req.body.email)) { - req.flash('warning', `${req.body.email} is not a valid email address. `); - res.redirect('/contact'); - } - - // Check for message - else if (req.body.message==='') { - req.flash('warning', `You need to enter a message. `); - res.redirect('/contact'); - } - - - // Passed validations - else { - - // Confirm captcha - request.post( 'https://www.google.com/recaptcha/api/siteverify', {form:{ - secret: env.recaptchaSecret, - response: req.body['g-recaptcha-response'], - remoteip: req.ip - }}, (err, response, body)=>{ - - // Check for errors - if (err){ - mw.throwErr(err,req); - res.redirect('/contact'); - } - if (response.statusCode!==200) { - let err = new Error('Bad response from reCaptcha service'); - mw.throwErr(err,req); - res.redirect('/contact'); - } - - // No errors - else { - - // Captcha failed - if (!JSON.parse(body).success){ - let err = new Error('Failed reCaptcha'); - mw.throwErr(err,req); - res.redirect('/contact'); - } - - // Captcha succeeded - else { - mail.send({ - from: `${req.body.name} <${req.body.email}>`, - to: `Tracman Contact `, - subject: req.body.subject||'A message', - text: req.body.message - }) - .then(()=>{ - req.flash('success', `Your message has been sent. `); - res.redirect(req.session.next || '/'); - }) - .catch((err)=>{ - mw.throwErr(err,req); - res.redirect('/contact'); - }); - } - - } - - }); - - } - -}); +.post('/', (req, res, next) => { + // Check email + if (req.body.email === '') { + req.flash('warning', `You need to enter an email address. `) + res.redirect('/contact') + } else if (!mw.validateEmail(req.body.email)) { + req.flash('warning', `${req.body.email} is not a valid email address. `) + res.redirect('/contact') + + // Check for message + } else if (req.body.message === '') { + req.flash('warning', `You need to enter a message. `) + res.redirect('/contact') + + // Passed validations + } else { + // Confirm captcha + request.post('https://www.google.com/recaptcha/api/siteverify', {form: { + secret: env.recaptchaSecret, + response: req.body['g-recaptcha-response'], + remoteip: req.ip + }}, (err, response, body) => { + // Check for errors + if (err) { + mw.throwErr(err, req) + res.redirect('/contact') + } + if (response.statusCode !== 200) { + let err = new Error('Bad response from reCaptcha service') + mw.throwErr(err, req) + res.redirect('/contact') + + // No errors + } else { + // Captcha failed + if (!JSON.parse(body).success) { + let err = new Error('Failed reCaptcha') + mw.throwErr(err, req) + res.redirect('/contact') + + // Captcha succeeded + } else { + mail.send({ + from: `${req.body.name} <${req.body.email}>`, + to: `Tracman Contact `, + subject: req.body.subject || 'A message', + text: req.body.message + }) + .then(() => { + req.flash('success', `Your message has been sent. `) + res.redirect(req.session.next || '/') + }) + .catch((err) => { + mw.throwErr(err, req) + res.redirect('/contact') + }) + } + } + }) + } +}) diff --git a/config/routes/index.js b/config/routes/index.js index 921b6fd..1bfa236 100644 --- a/config/routes/index.js +++ b/config/routes/index.js @@ -1,106 +1,96 @@ -'use strict'; +'use strict' -const router = require('express').Router(), - slug = require('slug'), - xss = require('xss'), - User = require('../models.js').user; +const router = require('express').Router() +const slug = require('slug') +const xss = require('xss') +const User = require('../models.js').user module.exports = router - - // Index - .get('/', (req,res,next)=>{ - res.render('index', {active:'home'}); - }) - - // Demo redirect - .get('/demo', (req,res,next)=>{ - res.redirect('/map/demo'); - }) - - // Help - .get('/help', (req,res)=>{ - res.render('help', {active:'help'}); - }) - - // Terms of Service and Privacy Policy - .get('/terms', (req,res)=>{ - res.render('terms', {active:'terms'}); - }) - .get('/privacy', (req,res)=>{ - res.render('privacy', {active:'privacy'}); - }) - - // robots.txt - .get('/robots.txt', (req,res)=>{ - res.type('text/plain'); - res.send("User-agent: *\n"+ - "Disallow: /map/*\n" - ); - }) - - // favicon.ico - //TODO: Just serve it - .get('/favicon.ico', (req,res)=>{ - res.redirect('/static/img/icon/by/16-32-48.ico'); - }) - - // Endpoint to validate forms - .get('/validate', (req,res,next)=>{ - - // Validate unique slug - if (req.query.slug) { - User.findOne({ slug: slug(req.query.slug) }) - .then( (existingUser)=>{ - if (existingUser && existingUser.id!==req.user.id) { - res.sendStatus(400); - } - else { res.sendStatus(200); } - }) - .catch( (err)=>{ - console.error(err); - res.sendStatus(500); - }); - } - - // Validate unique email - else if (req.query.email) { - User.findOne({ email: req.query.email }) - .then( (existingUser)=>{ - if (existingUser && existingUser.id!==req.user.id) { - res.sendStatus(400); - } - else { res.sendStatus(200); } - }) - .catch( (err)=>{ - console.error(err); - res.sendStatus(500); - }); - } - - // Create slug - else if (req.query.slugify) { - res.send(slug(xss(req.query.slugify))); - } - - // Sanitize for XSS - else if (req.query.xss) { - res.send(xss(req.query.xss)); - } - - // 404 - else { next(); } - - }) - - // Link to androidapp in play store - .get('/android', (req,res)=>{ - res.redirect('https://play.google.com/store/apps/details?id=us.keithirwin.tracman'); - }) - - // Link to iphone app in the apple store - // ... maybe someday - .get('/ios', (req,res)=>{ - res.redirect('/help#why-is-there-no-ios-app'); - }) -; + // Index + .get('/', (req, res, next) => { + res.render('index', {active: 'home'}) + }) + + // Demo redirect + .get('/demo', (req, res, next) => { + res.redirect('/map/demo') + }) + + // Help + .get('/help', (req, res) => { + res.render('help', {active: 'help'}) + }) + + // Terms of Service and Privacy Policy + .get('/terms', (req, res) => { + res.render('terms', {active: 'terms'}) + }) + .get('/privacy', (req, res) => { + res.render('privacy', {active: 'privacy'}) + }) + + // robots.txt + .get('/robots.txt', (req, res) => { + res.type('text/plain') + res.send('User-agent: *\n' + + 'Disallow: /map/*\n' + ) + }) + + // favicon.ico + // TODO: Just serve it + .get('/favicon.ico', (req, res) => { + res.redirect('/static/img/icon/by/16-32-48.ico') + }) + + // Endpoint to validate forms + .get('/validate', (req, res, next) => { + // Validate unique slug + if (req.query.slug) { + User.findOne({ slug: slug(req.query.slug) }) + .then((existingUser) => { + if (existingUser && existingUser.id !== req.user.id) { + res.sendStatus(400) + } else { res.sendStatus(200) } + }) + .catch((err) => { + console.error(err) + res.sendStatus(500) + }) + + // Validate unique email + } else if (req.query.email) { + User.findOne({ email: req.query.email }) + .then((existingUser) => { + if (existingUser && existingUser.id !== req.user.id) { + res.sendStatus(400) + } else { res.sendStatus(200) } + }) + .catch((err) => { + console.error(err) + res.sendStatus(500) + }) + + // Create slug + } else if (req.query.slugify) { + res.send(slug(xss(req.query.slugify))) + + // Sanitize for XSS + } else if (req.query.xss) { + res.send(xss(req.query.xss)) + + // 404 + } else { next() } + }) + + // Link to androidapp in play store + .get('/android', (req, res) => { + res.redirect('https://play.google.com/store/apps/details?id=us.keithirwin.tracman') + }) + + // Link to iphone app in the apple store + // ... maybe someday + .get('/ios', (req, res) => { + res.redirect('/help#why-is-there-no-ios-app') + }) diff --git a/config/routes/map.js b/config/routes/map.js index ad5f140..cda2973 100644 --- a/config/routes/map.js +++ b/config/routes/map.js @@ -1,79 +1,76 @@ -'use strict'; +'use strict' -const router = require('express').Router(), - mw = require('../middleware.js'), - env = require('../env/env.js'), - User = require('../models.js').user; - +const router = require('express').Router() +const mw = require('../middleware.js') +const env = require('../env/env.js') +const User = require('../models.js').user // Redirect to real slug -router.get('/', mw.ensureAuth, (req,res)=>{ - if (req.query.new){ - res.redirect(`/map/${req.user.slug}?new=1`); - } - else { - res.redirect(`/map/${req.user.slug}`); - } -}); +router.get('/', mw.ensureAuth, (req, res) => { + if (req.query.new) { + res.redirect(`/map/${req.user.slug}?new=1`) + } else { + res.redirect(`/map/${req.user.slug}`) + } +}) // Demo -router.get('/demo', (req,res,next)=>{ - res.render('map', { - active: 'demo', - mapuser: { - _id: 'demo', - name: 'Demo', - last: { - lat: 40.1165853, - lon: -87.5417312, - dir: 249.0, - spd: 19.015747 - }, - settings: { - marker: 'marker-red', - showAlt: false, - showTemp: false, - showSpeed: false, - showScale: false, - showStreetview: true, - defaultZoom: 13, - defaultMap: 'road', - units: 'standard' - }, - }, - mapApi: env.googleMapsAPI, - user: req.user, - noFooter: '1', - noHeader: (req.query.noheader)?req.query.noheader.match(/\d/)[0]:0, - disp: (req.query.disp)?req.query.disp.match(/\d/)[0]:2, // 0=map, 1=streetview, 2=both - newuserurl: (req.query.new)? env.url+'/map/'+req.params.slug : '' - }); -}); +router.get('/demo', (req, res, next) => { + res.render('map', { + active: 'demo', + mapuser: { + _id: 'demo', + name: 'Demo', + last: { + lat: 40.1165853, + lon: -87.5417312, + dir: 249.0, + spd: 19.015747 + }, + settings: { + marker: 'marker-red', + showAlt: false, + showTemp: false, + showSpeed: false, + showScale: false, + showStreetview: true, + defaultZoom: 13, + defaultMap: 'road', + units: 'standard' + } + }, + mapApi: env.googleMapsAPI, + user: req.user, + noFooter: '1', + noHeader: (req.query.noheader) ? req.query.noheader.match(/\d/)[0] : 0, + disp: (req.query.disp) ? req.query.disp.match(/\d/)[0] : 2, // 0=map, 1=streetview, 2=both + newuserurl: (req.query.new) ? env.url + '/map/' + req.params.slug : '' + }) +}) // Show map -router.get('/:slug?', (req,res,next)=>{ - - User.findOne({slug:req.params.slug}) - .then( (mapuser)=>{ - if (!mapuser){ next(); } //404 - else { - var active = ''; // For header nav - if (req.user && req.user.id===mapuser.id){ active='map'; } - res.render('map', { - active: active, - mapuser: mapuser, - mapApi: env.googleMapsAPI, - user: req.user, - noFooter: '1', - noHeader: (req.query.noheader)?req.query.noheader.match(/\d/)[0]:0, - disp: (req.query.disp)?req.query.disp.match(/\d/)[0]:2, // 0=map, 1=streetview, 2=both - newuserurl: (req.query.new)? env.url+'/map/'+req.params.slug : '' - }); - } - }).catch( (err)=>{ - mw.throwErr(err,req); - }); - -}); +router.get('/:slug?', (req, res, next) => { + User.findOne({slug: req.params.slug}) + .then((mapuser) => { + if (!mapuser) { + next() // 404 + } else { + var active = '' // For header nav + if (req.user && req.user.id === mapuser.id) { active = 'map' } + res.render('map', { + active: active, + mapuser: mapuser, + mapApi: env.googleMapsAPI, + user: req.user, + noFooter: '1', + noHeader: (req.query.noheader) ? req.query.noheader.match(/\d/)[0] : 0, + disp: (req.query.disp) ? req.query.disp.match(/\d/)[0] : 2, // 0=map, 1=streetview, 2=both + newuserurl: (req.query.new) ? env.url + '/map/' + req.params.slug : '' + }) + } + }).catch((err) => { + mw.throwErr(err, req) + }) +}) -module.exports = router; +module.exports = router diff --git a/config/routes/settings.js b/config/routes/settings.js index 3034836..8c87ec1 100644 --- a/config/routes/settings.js +++ b/config/routes/settings.js @@ -1,359 +1,315 @@ -'use strict'; - -const slug = require('slug'), - xss = require('xss'), - zxcvbn = require('zxcvbn'), - moment = require('moment'), - mw = require('../middleware.js'), - User = require('../models.js').user, - mail = require('../mail.js'), - env = require('../env/env.js'), - debug = require('debug')('tracman-settings'), - router = require('express').Router(); - +'use strict' +const slug = require('slug') +const xss = require('xss') +const zxcvbn = require('zxcvbn') +const moment = require('moment') +const mw = require('../middleware.js') +const User = require('../models.js').user +const mail = require('../mail.js') +const env = require('../env/env.js') +const debug = require('debug')('tracman-settings') +const router = require('express').Router() // Settings form router.route('/') - .all( mw.ensureAuth, (req,res,next)=>{ - next(); - } ) + .all(mw.ensureAuth, (req, res, next) => { + next() + }) - // Get settings form - .get( (req,res)=>{ - res.render('settings', {active:'settings'}); - } ) + // Get settings form + .get((req, res) => { + res.render('settings', {active: 'settings'}) + }) - // Set new settings - .post( (req,res,next)=>{ - - // Validate email - const checkEmail = new Promise( (resolve,reject)=>{ - - // Check validity - if (!mw.validateEmail(req.body.email)) { - req.flash('warning', `${req.body.email} is not a valid email address. `); - resolve(); - } - - // Check if unchanged - else if (req.user.email===req.body.email) { - resolve(); - } - - // Check uniqueness - else { - User.findOne({ email: req.body.email }) - .then( (existingUser)=>{ - - // Not unique! - if (existingUser && existingUser.id!==req.user.id) { - debug("Email not unique!"); - req.flash('warning', `That email, ${req.body.email}, is already in use by another user! `); - resolve(); - } - - // It's unique - else { - debug("Email is unique"); - req.user.newEmail = req.body.email; - - // Create token - debug(`Creating email token...`); - req.user.createEmailToken((err,token)=>{ - if (err){ reject(err); } - - // Send token to user by email - debug(`Mailing new email token to ${req.body.email}...`); - mail.send({ - to: `"${req.user.name}" <${req.body.email}>`, - from: mail.noReply, - subject: 'Confirm your new email address for Tracman', - text: mail.text(`A request has been made to change your Tracman email address. If you did not initiate this request, please disregard it. \n\nTo confirm your email, follow this link:\n${env.url}/settings/email/${token}. `), - html: mail.html(`

A request has been made to change your Tracman email address. If you did not initiate this request, please disregard it.

To confirm your email, follow this link:
${env.url}/settings/email/${token}.

`) - }) - .then( ()=>{ - req.flash('warning',`An email has been sent to ${req.body.email}. Check your inbox to confirm your new email address. `); - resolve(); - }) - .catch(reject); - - }); - - } - - }) - .catch(reject); - } - - }); - - // Validate slug - const checkSlug = new Promise( (resolve,reject)=>{ - - // Check existence - if (req.body.slug==='') { - req.flash('warning', `You must supply a slug. `); - resolve(); - } - - // Check if unchanged - else if (req.user.slug===slug(xss(req.body.slug))) { - resolve(); - } - - // Check uniqueness - else { - - User.findOne({ slug: req.body.slug }) - .then( (existingUser)=>{ - - // Not unique! - if (existingUser && existingUser.id!==req.user.id) { - req.flash('warning', `That slug, ${req.body.slug}, is already in use by another user! `); - } - - // It's unique - else { - req.user.slug = slug(xss(req.body.slug)); - } - - }) - .then(resolve) - .catch(reject); - - } - - }); - - // Set settings when done - Promise.all([checkEmail, checkSlug]) - .then( ()=>{ - debug('Setting settings... '); - - // Set values - req.user.name = xss(req.body.name); - req.user.settings = { - units: req.body.units, - defaultMap: req.body.map, - defaultZoom: req.body.zoom, - showScale: (req.body.showScale)?true:false, - showSpeed: (req.body.showSpeed)?true:false, - showAlt: (req.body.showAlt)?true:false, - showStreetview: (req.body.showStreet)?true:false, - marker: req.body.marker - }; - - // Save user and send response - debug(`Saving new settings for user ${req.user.name}...`); - req.user.save() - .then( ()=>{ - debug(`DONE! Redirecting user...`); - req.flash('success', 'Settings updated. '); - res.redirect('/settings'); - }) - .catch( (err)=>{ - mw.throwErr(err,req); - res.redirect('/settings'); - }); - - }) - .catch( (err)=>{ - mw.throwErr(err,req); - res.redirect('/settings'); - }); - - } ); + // Set new settings + .post((req, res, next) => { + // Validate email + const checkEmail = new Promise((resolve, reject) => { + // Check validity + if (!mw.validateEmail(req.body.email)) { + req.flash('warning', `${req.body.email} is not a valid email address. `) + resolve() + + // Check if unchanged + } else if (req.user.email === req.body.email) { + resolve() + + // Check uniqueness + } else { + User.findOne({ email: req.body.email }) + .then((existingUser) => { + // Not unique! + if (existingUser && existingUser.id !== req.user.id) { + debug('Email not unique!') + req.flash('warning', `That email, ${req.body.email}, is already in use by another user! `) + resolve() + + // It's unique + } else { + debug('Email is unique') + req.user.newEmail = req.body.email + + // Create token + debug(`Creating email token...`) + req.user.createEmailToken((err, token) => { + if (err) { reject(err) } + + // Send token to user by email + debug(`Mailing new email token to ${req.body.email}...`) + mail.send({ + to: `"${req.user.name}" <${req.body.email}>`, + from: mail.noReply, + subject: 'Confirm your new email address for Tracman', + text: mail.text(`A request has been made to change your Tracman email address. If you did not initiate this request, please disregard it. \n\nTo confirm your email, follow this link:\n${env.url}/settings/email/${token}. `), + html: mail.html(`

A request has been made to change your Tracman email address. If you did not initiate this request, please disregard it.

To confirm your email, follow this link:
${env.url}/settings/email/${token}.

`) + }) + .then(() => { + req.flash('warning', `An email has been sent to ${req.body.email}. Check your inbox to confirm your new email address. `) + resolve() + }) + .catch(reject) + }) + } + }) + .catch(reject) + } + }) + + // Validate slug + const checkSlug = new Promise((resolve, reject) => { + // Check existence + if (req.body.slug === '') { + req.flash('warning', `You must supply a slug. `) + resolve() + + // Check if unchanged + } else if (req.user.slug === slug(xss(req.body.slug))) { + resolve() + + // Check uniqueness + } else { + User.findOne({ slug: req.body.slug }) + .then((existingUser) => { + // Not unique! + if (existingUser && existingUser.id !== req.user.id) { + req.flash('warning', `That slug, ${req.body.slug}, is already in use by another user! `) + + // It's unique + } else { + req.user.slug = slug(xss(req.body.slug)) + } + }) + .then(resolve) + .catch(reject) + } + }) + + // Set settings when done + Promise.all([checkEmail, checkSlug]) + .then(() => { + debug('Setting settings... ') + + // Set values + req.user.name = xss(req.body.name) + req.user.settings = { + units: req.body.units, + defaultMap: req.body.map, + defaultZoom: req.body.zoom, + showScale: !!(req.body.showScale), + showSpeed: !!(req.body.showSpeed), + showAlt: !!(req.body.showAlt), + showStreetview: !!(req.body.showStreet), + marker: req.body.marker + } + + // Save user and send response + debug(`Saving new settings for user ${req.user.name}...`) + req.user.save() + .then(() => { + debug(`DONE! Redirecting user...`) + req.flash('success', 'Settings updated. ') + res.redirect('/settings') + }) + .catch((err) => { + mw.throwErr(err, req) + res.redirect('/settings') + }) + }) + .catch((err) => { + mw.throwErr(err, req) + res.redirect('/settings') + }) + }) // Delete account -router.get('/delete', (req,res)=>{ - User.findByIdAndRemove(req.user) - .then( ()=>{ - req.flash('success', 'Your account has been deleted. '); - res.redirect('/'); - }) - .catch( (err)=>{ - mw.throwErr(err,req); - res.redirect('/settings'); - }); -}); +router.get('/delete', (req, res) => { + User.findByIdAndRemove(req.user) + .then(() => { + req.flash('success', 'Your account has been deleted. ') + res.redirect('/') + }) + .catch((err) => { + mw.throwErr(err, req) + res.redirect('/settings') + }) +}) // Confirm email address -router.get('/email/:token', mw.ensureAuth, (req,res,next)=>{ - - // Check token - if ( req.user.emailToken===req.params.token) { - - // Set new email - req.user.email = req.user.newEmail; - req.user.save() - - // Delete token and newEmail - .then( ()=>{ - req.user.emailToken = undefined; - req.user.newEmail = undefined; - req.user.save(); - }) - - // Report success - .then( ()=>{ - req.flash('success',`Your email has been set to ${req.user.email}. `); - res.redirect('/settings'); - }) - - .catch( (err)=>{ - mw.throwErr(err,req); - res.redirect(req.session.next||'/settings'); - }); - - } - - // Invalid token - else { - req.flash('danger', 'Email confirmation token is invalid. '); - res.redirect('/settings'); - } - -} ); +router.get('/email/:token', mw.ensureAuth, (req, res, next) => { + // Check token + if (req.user.emailToken === req.params.token) { + // Set new email + req.user.email = req.user.newEmail + req.user.save() + + // Delete token and newEmail + .then(() => { + req.user.emailToken = undefined + req.user.newEmail = undefined + req.user.save() + }) + + // Report success + .then(() => { + req.flash('success', `Your email has been set to ${req.user.email}. `) + res.redirect('/settings') + }) + + .catch((err) => { + mw.throwErr(err, req) + res.redirect(req.session.next || '/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(); - } ) + .all(mw.ensureAuth, (req, res, next) => { + next() + }) - // Email user a token, proceed at /password/:token - .get( (req,res,next)=>{ + // Email user a token, proceed at /password/:token + .get((req, res, next) => { + // Create token for password change + req.user.createPassToken((err, token, expires) => { + if (err) { + mw.throwErr(err, req) + res.redirect((req.user) ? '/settings' : '/login') + } else { + // 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' - // Create token for password change - req.user.createPassToken( (err,token,expires)=>{ - if (err){ - mw.throwErr(err,req); - res.redirect((req.user)?'/settings':'/login'); - } - else { - - // 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"; - - // Confirm password change request by email. - 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}/settings/password/${token}. \n\nThis 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}/settings/password/${token}.

This request will expire at ${expirationTimeString}.

`) - }) - .then( ()=>{ - // 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}). `); - res.redirect((req.user)?'/settings':'/login'); - }) - .catch( (err)=>{ - mw.throwErr(err,req); - res.redirect((req.user)?'/settings':'/login'); - }); - - } - }); - - } ); + // Confirm password change request by email. + 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}/settings/password/${token}. \n\nThis 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}/settings/password/${token}.

This request will expire at ${expirationTimeString}.

`) + }) + .then(() => { + // 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}). `) + res.redirect((req.user) ? '/settings' : '/login') + }) + .catch((err) => { + mw.throwErr(err, req) + res.redirect((req.user) ? '/settings' : '/login') + }) + } + }) + }) router.route('/password/:token') - // Check token - .all( (req,res,next)=>{ - debug('/settings/password/:token .all() called'); - User - .findOne({'auth.passToken': req.params.token}) - .where('auth.passTokenExpires').gt(Date.now()) - .then((user) => { - 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'); - }); - } ) + // Check token + .all((req, res, next) => { + debug('/settings/password/:token .all() called') + User + .findOne({'auth.passToken': req.params.token}) + .where('auth.passTokenExpires').gt(Date.now()) + .then((user) => { + 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('/settings/password/:token .get() called'); - res.render('password'); - } ) - - // Set new password - .post( (req,res,next)=>{ - - // Validate password - let zxcvbnResult = zxcvbn(req.body.password); - if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 864000) { // Less than ten days - mw.throwErr(new Error(`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(`/settings/password/${req.params.token}`); - } - - else { - - // Create hashed password and save to db - res.locals.passwordUser.generateHashedPassword( req.body.password, (err)=>{ - if (err){ - mw.throwErr(err,req); - res.redirect(`/password/${req.params.token}`); - } - - // User changed password - else if (req.user) { - req.flash('success', 'Your password has been changed. '); - res.redirect('/settings'); - } - - // New user created password - else { - req.flash('success', 'Password set. You can use it to log in now. '); - res.redirect('/login?next=/map?new=1'); - } - - } ); - - } - - } ); + // Show password change form + .get((req, res) => { + debug('/settings/password/:token .get() called') + res.render('password') + }) + // Set new password + .post((req, res, next) => { + // Validate password + let zxcvbnResult = zxcvbn(req.body.password) + if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 864000) { // Less than ten days + mw.throwErr(new Error(`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(`/settings/password/${req.params.token}`) + } else { + // Create hashed password and save to db + res.locals.passwordUser.generateHashedPassword(req.body.password, (err) => { + if (err) { + mw.throwErr(err, req) + res.redirect(`/password/${req.params.token}`) + + // User changed password + } else if (req.user) { + req.flash('success', 'Your password has been changed. ') + res.redirect('/settings') + + // New user created password + } else { + req.flash('success', 'Password set. You can use it to log in now. ') + res.redirect('/login?next=/map?new=1') + } + }) + } + }) // Tracman pro router.route('/pro') - .all( mw.ensureAuth, (req,res,next)=>{ - next(); - } ) + .all(mw.ensureAuth, (req, res, next) => { + next() + }) - // Get info about pro - .get( (req,res,next)=>{ - res.render('pro'); - } ) + // Get info about pro + .get((req, res, next) => { + res.render('pro') + }) - // Join Tracman pro - .post( (req,res)=>{ - User.findByIdAndUpdate(req.user.id, - {$set:{ isPro:true }}) - .then( (user)=>{ - req.flash('success','You have been signed up for pro. '); - res.redirect('/settings'); - }) - .catch( (err)=>{ - mw.throwErr(err,req); - res.redirect('/settings/pro'); - }); - } ); + // Join Tracman pro + .post((req, res) => { + User.findByIdAndUpdate(req.user.id, + {$set: { isPro: true }}) + .then((user) => { + req.flash('success', 'You have been signed up for pro. ') + res.redirect('/settings') + }) + .catch((err) => { + mw.throwErr(err, req) + res.redirect('/settings/pro') + }) + }) -module.exports = router; +module.exports = router diff --git a/config/routes/test.js b/config/routes/test.js index 86b3a07..b2d2e5c 100644 --- a/config/routes/test.js +++ b/config/routes/test.js @@ -1,52 +1,51 @@ -'use strict'; +'use strict' const router = require('express').Router(), - zxcvbn = require('zxcvbn'), + zxcvbn = require('zxcvbn'), mw = require('../middleware.js'), - mail = require('../mail.js'); + mail = require('../mail.js') router - .get('/mail', (req,res,next)=>{ - mail.send({ - to: `"Keith Irwin" `, - from: mail.noReply, - subject: 'Test email', - text: mail.text("Looks like everything's working! "), - html: mail.html("

Looks like everything's working!

") - }) - .then(()=>{ - console.log("Test email should have sent..."); - res.sendStatus(200); - }) - .catch((err)=>{ - mw.throwErr(err,req); - res.sendStatus(500); - }); - }) - - .get('/password', (req,res)=>{ - res.render('password'); - }) - .post('/password', (req,res,next)=>{ - let zxcvbnResult = zxcvbn(req.body.password); - if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 864000) { // Less than ten days - let err = new Error(`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. `); - mw.throwErr(err,req); - next(err); - } - else { - res.sendStatus(200); - } - }) - - .get('/settings', (req,res)=>{ - res.render('settings'); - }) - .post('/settings', (req,res)=>{ - - //TODO: Test validation here? - - }); - -module.exports = router; \ No newline at end of file + .get('/mail', (req, res, next) => { + mail.send({ + to: `"Keith Irwin" `, + from: mail.noReply, + subject: 'Test email', + text: mail.text("Looks like everything's working! "), + html: mail.html("

Looks like everything's working!

") + }) + .then(() => { + console.log('Test email should have sent...') + res.sendStatus(200) + }) + .catch((err) => { + mw.throwErr(err, req) + res.sendStatus(500) + }) + }) + + .get('/password', (req, res) => { + res.render('password') + }) + .post('/password', (req, res, next) => { + let zxcvbnResult = zxcvbn(req.body.password) + if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 864000) { // Less than ten days + let err = new Error(`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. `) + mw.throwErr(err, req) + next(err) + } else { + res.sendStatus(200) + } + }) + + .get('/settings', (req, res) => { + res.render('settings') + }) + .post('/settings', (req, res) => { + + // TODO: Test validation here? + + }) + +module.exports = router diff --git a/config/sockets.js b/config/sockets.js index ee16582..91e259b 100644 --- a/config/sockets.js +++ b/config/sockets.js @@ -1,124 +1,114 @@ -'use strict'; +'use strict' // Imports -const debug = require('debug')('tracman-sockets'), - User = require('./models.js').user; +const debug = require('debug')('tracman-sockets') +const User = require('./models.js').user // Check for tracking clients -function checkForUsers(io, user) { - debug(`Checking for clients receiving updates for ${user}`); - - // Checks if any sockets are getting updates for this user - if (Object.values(io.sockets.connected).some( (socket)=>{ - return socket.gets===user; - })) { - debug(`Activating updates for ${user}.`); - io.to(user).emit('activate','true'); - } else { - debug(`Deactivating updates for ${user}.`); - io.to(user).emit('activate', 'false'); - } +function checkForUsers (io, user) { + debug(`Checking for clients receiving updates for ${user}`) + + // Checks if any sockets are getting updates for this user + if (Object.values(io.sockets.connected).some((socket) => { + return socket.gets === user + })) { + debug(`Activating updates for ${user}.`) + io.to(user).emit('activate', 'true') + } else { + debug(`Deactivating updates for ${user}.`) + io.to(user).emit('activate', 'false') + } } module.exports = { - - checkForUsers: checkForUsers, - - init: (io)=>{ - io.on('connection', (socket)=>{ - debug(`${socket.id} connected.`); - - // Set a few variables - //socket.ip = socket.client.request.headers['x-real-ip']; - //socket.ua = socket.client.request.headers['user-agent']; - - // Log and errors - socket.on('log', (text)=>{ - debug(`LOG: ${text}`); - }); - socket.on('error', (err)=>{ console.error('❌', err.stack); }); - - // This socket can set location (app) - socket.on('can-set', (userId)=>{ - debug(`${socket.id} can set updates for ${userId}.`); - socket.join(userId, ()=>{ - debug(`${socket.id} joined ${userId}`); - }); - checkForUsers( io, userId ); - }); - - // This socket can receive location (map) - socket.on('can-get', (userId)=>{ - socket.gets = userId; - debug(`${socket.id} can get updates for ${userId}.`); - socket.join(userId, ()=>{ - debug(`${socket.id} joined ${userId}`); - socket.to(userId).emit('activate', 'true'); - }); - }); - - // Set location - socket.on('set', (loc)=>{ - debug(`${socket.id} set location for ${loc.usr}`); - debug(`Location was set to: ${JSON.stringify(loc)}`); - - // Get android timestamp or use server timestamp - if (loc.ts){ loc.tim = Date(loc.ts); } - else { loc.tim = Date.now(); } - // Check for user and sk32 token - if (!loc.usr){ - console.error("❌", new Error(`Recieved an update from ${socket.ip} without a usr!`).message); - } - else if (!loc.tok){ - console.error("❌", new Error(`Recieved an update from ${socket.ip} for usr ${loc.usr} without an sk32!`).message); - } - else { - - // Get loc.usr - User.findById(loc.usr) - .where('sk32').equals(loc.tok) - .then( (user)=>{ - if (!user){ - console.error("❌", new Error(`Recieved an update from ${socket.ip} for ${loc.usr} with tok of ${loc.tok}, but no such user was found in the db!`).message); - } - else { - - // Broadcast location - io.to(loc.usr).emit('get', loc); - debug(`Broadcasting ${loc.lat}, ${loc.lon} to ${loc.usr}`); - - // Save in db as last seen - user.last = { - lat: parseFloat(loc.lat), - lon: parseFloat(loc.lon), - dir: parseFloat(loc.dir||0), - spd: parseFloat(loc.spd||0), - time: loc.tim - }; - user.save() - .catch( (err)=>{ console.error("❌", err.stack); }); + checkForUsers: checkForUsers, - } - }) - .catch( (err)=>{ console.error("❌", err.stack); }); - - } - }); - - // Shutdown (check for remaining clients) - socket.on('disconnect', (reason)=>{ - debug(`${socket.id} disconnected because of a ${reason}.`); - - // Check if client was receiving updates - if (socket.gets){ - debug(`${socket.id} left ${socket.gets}`); - checkForUsers( io, socket.gets ); - } - - }); - - }); - } - -}; \ No newline at end of file + init: (io) => { + io.on('connection', (socket) => { + debug(`${socket.id} connected.`) + + // Set a few variables + // socket.ip = socket.client.request.headers['x-real-ip']; + // socket.ua = socket.client.request.headers['user-agent']; + + // Log and errors + socket.on('log', (text) => { + debug(`LOG: ${text}`) + }) + socket.on('error', (err) => { console.error('❌', err.stack) }) + + // This socket can set location (app) + socket.on('can-set', (userId) => { + debug(`${socket.id} can set updates for ${userId}.`) + socket.join(userId, () => { + debug(`${socket.id} joined ${userId}`) + }) + checkForUsers(io, userId) + }) + + // This socket can receive location (map) + socket.on('can-get', (userId) => { + socket.gets = userId + debug(`${socket.id} can get updates for ${userId}.`) + socket.join(userId, () => { + debug(`${socket.id} joined ${userId}`) + socket.to(userId).emit('activate', 'true') + }) + }) + + // Set location + socket.on('set', (loc) => { + debug(`${socket.id} set location for ${loc.usr}`) + debug(`Location was set to: ${JSON.stringify(loc)}`) + + // Get android timestamp or use server timestamp + if (loc.ts) { loc.tim = Date(loc.ts) } else { loc.tim = Date.now() } + + // Check for user and sk32 token + if (!loc.usr) { + console.error('❌', new Error(`Recieved an update from ${socket.ip} without a usr!`).message) + } else if (!loc.tok) { + console.error('❌', new Error(`Recieved an update from ${socket.ip} for usr ${loc.usr} without an sk32!`).message) + } else { + // Get loc.usr + User.findById(loc.usr) + .where('sk32').equals(loc.tok) + .then((user) => { + if (!user) { + console.error('❌', new Error(`Recieved an update from ${socket.ip} for ${loc.usr} with tok of ${loc.tok}, but no such user was found in the db!`).message) + } else { + // Broadcast location + io.to(loc.usr).emit('get', loc) + debug(`Broadcasting ${loc.lat}, ${loc.lon} to ${loc.usr}`) + + // Save in db as last seen + user.last = { + lat: parseFloat(loc.lat), + lon: parseFloat(loc.lon), + dir: parseFloat(loc.dir || 0), + spd: parseFloat(loc.spd || 0), + time: loc.tim + } + user.save() + .catch((err) => { console.error('❌', err.stack) }) + } + }) + .catch((err) => { console.error('❌', err.stack) }) + } + }) + + // Shutdown (check for remaining clients) + socket.on('disconnect', (reason) => { + debug(`${socket.id} disconnected because of a ${reason}.`) + + // Check if client was receiving updates + if (socket.gets) { + debug(`${socket.id} left ${socket.gets}`) + checkForUsers(io, socket.gets) + } + }) + }) + } + +} diff --git a/package.json b/package.json index cca0a24..c26fedf 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "nodemon": "nodemon --ignore 'static/**/*.min.*' server.js", "update": "sudo npm update && sudo npm prune", "minify": "minify --template .{{filename}}.min.{{ext}} --clean static/css*", - "build": "standard && ./node_modules/.bin/webpack --config webpack.config.js", + "build": "./node_modules/.bin/webpack --config webpack.config.js", "subuild": "sudo ./node_modules/.bin/webpack --config webpack.config.js" }, "repository": "Tracman-org/Server", diff --git a/server.js b/server.js index 4d596c9..d2f9357 100755 --- a/server.js +++ b/server.js @@ -1,199 +1,181 @@ -'use strict'; +'use strict' /* IMPORTS */ -const - express = require('express'), - bodyParser = require('body-parser'), - expressValidator = require('express-validator'), - cookieParser = require('cookie-parser'), - cookieSession = require('cookie-session'), - debug = require('debug')('tracman-server'), - mongoose = require('mongoose'), - nunjucks = require('nunjucks'), - passport = require('passport'), - flash = require('connect-flash-plus'), - env = require('./config/env/env.js'), - User = require('./config/models.js').user, - mail = require('./config/mail.js'), - demo = require('./config/demo.js'), - app = express(), - http = require('http').Server(app), - io = require('socket.io')(http), - sockets = require('./config/sockets.js'); +const express = require('express') +const bodyParser = require('body-parser') +const expressValidator = require('express-validator') +const cookieParser = require('cookie-parser') +const cookieSession = require('cookie-session') +const debug = require('debug')('tracman-server') +const mongoose = require('mongoose') +const nunjucks = require('nunjucks') +const passport = require('passport') +const flash = require('connect-flash-plus') +const env = require('./config/env/env.js') +const User = require('./config/models.js').user +const mail = require('./config/mail.js') +const demo = require('./config/demo.js') +const app = express() +const http = require('http').Server(app) +const io = require('socket.io')(http) +const sockets = require('./config/sockets.js') +/* SETUP */ +/* Database */ { + // Setup with native ES6 promises + mongoose.Promise = global.Promise -/* SETUP */ { - - /* Database */ { - - // Setup with native ES6 promises - mongoose.Promise = global.Promise; - - // Connect to database - mongoose.connect(env.mongoSetup, { - server:{socketOptions:{ - keepAlive:1, connectTimeoutMS:30000 }}, - replset:{socketOptions:{ - keepAlive:1, connectTimeoutMS:30000 }} - }) - .then( ()=>{ console.log(`💿 Mongoose connected to mongoDB`); }) - .catch( (err)=>{ console.error(`❌ ${err.stack}`); }); - - } - - /* Templates */ { - nunjucks.configure(__dirname+'/views', { - autoescape: true, - express: app - }); - app.set('view engine','html'); - } - - /* Session */ { - app.use(cookieParser(env.cookie)); - app.use(cookieSession({ - cookie: {maxAge:60000}, - secret: env.session, - saveUninitialized: true, - resave: true - })); - app.use(bodyParser.json()); - app.use(bodyParser.urlencoded({ - extended: true - })); - app.use(expressValidator()); - app.use(flash()); - } - - /* Auth */ { - require('./config/passport.js')(passport); - app.use(passport.initialize()); - app.use(passport.session()); - } - - /* Routes */ { - - // Static files (keep this before default locals) - app.use('/static', express.static( __dirname+'/static', {dotfiles:'allow'} )); - - // Default locals available to all views (keep this after static files) - app.get( '*', (req,res,next)=>{ - - // Path for redirects - let nextPath = ((req.query.next)?req.query.next: req.path.substring(0,req.path.indexOf('#')) || req.path ); - if ( nextPath.substring(0,6)!=='/login' && nextPath.substring(0,7)!=='signup' && nextPath.substring(0,7)!=='/logout' && nextPath.substring(0,7)!=='/static' && nextPath.substring(0,6)!=='/admin' ){ - req.session.next = nextPath+'#'; - debug(`Set redirect path to ${nextPath}#`); - } - - // User account - res.locals.user = req.user; - - // Flash messages - res.locals.successes = req.flash('success'); - res.locals.dangers = req.flash('danger'); - res.locals.warnings = req.flash('warning'); - - next(); - } ); - - // Auth routes - require('./config/routes/auth.js')(app, passport); - - // Main routes - app.use( '/', require('./config/routes/index.js') ); - - // Contact form - app.use( '/contact', require('./config/routes/contact.js') ); - - // Settings - app.use( '/settings', require('./config/routes/settings.js') ); - - // Map - app.use( ['/map','/trac'], require('./config/routes/map.js') ); - - // Site administration - app.use( '/admin', require('./config/routes/admin.js') ); - - // Testing - if (env.mode == 'development') { - app.use( '/test', require('./config/routes/test.js' ) ); - } - - } - - /* Errors */ { - - // Catch-all for 404s - app.use( (req,res,next)=>{ - if (!res.headersSent) { - var err = new Error(`Not found: ${req.url}`); - err.status = 404; - next(err); - } - } ); - - // Production handlers - if (env.mode!=='development') { - app.use( (err,req,res,next)=>{ - if (err.status!==404&&err.status!==401){ console.error(`❌ ${err.stack}`); } - if (res.headersSent) { return next(err); } - res.status(err.status||500); - res.render('error', { - code: err.status||500, - message: (err.status<=499)?err.message:"Server error" - }); - } ); - } - - // Development handlers - else { - app.use( (err,req,res,next)=>{ - if (err.status!==404) { console.error(`❌ ${err.stack}`); } - if (res.headersSent) { return next(err); } - res.status(err.status||500); - res.render('error', { - code: err.status||500, - message: err.message, - stack: err.stack - }); - } ); - } - - } - - /* Sockets */ { - sockets.init(io); - } - + // Connect to database + mongoose.connect(env.mongoSetup, { + server: {socketOptions: { + keepAlive: 1, connectTimeoutMS: 30000 }}, + replset: {socketOptions: { + keepAlive: 1, connectTimeoutMS: 30000 }} + }) + .then(() => { console.log(`💿 Mongoose connected to mongoDB`) }) + .catch((err) => { console.error(`❌ ${err.stack}`) }) } -/* RUNTIME */ { - console.log('🖥 Starting Tracman server...'); - - // Test SMTP server - mail.verify(); - - // Listen - http.listen( env.port, ()=>{ - console.log(`🌐 Listening in ${env.mode} mode on port ${env.port}... `); - - // Check for clients for each user - User.find({}) - .then( (users)=>{ - users.forEach( (user)=>{ - sockets.checkForUsers( io, user.id ); - }); - }) - .catch( (err)=>{ - console.error(`❌ ${err.stack}`); - }); - - // Start transmitting demo - demo(io); - - }); - +/* Templates */ { + nunjucks.configure(__dirname + '/views', { + autoescape: true, + express: app + }) + app.set('view engine', 'html') } -module.exports = app; +/* Session */ { + app.use(cookieParser(env.cookie)) + app.use(cookieSession({ + cookie: {maxAge: 60000}, + secret: env.session, + saveUninitialized: true, + resave: true + })) + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ + extended: true + })) + app.use(expressValidator()) + app.use(flash()) +} + +/* Auth */ { + require('./config/passport.js')(passport) + app.use(passport.initialize()) + app.use(passport.session()) +} + +/* Routes */ { + // Static files (keep this before default locals) + app.use('/static', express.static(__dirname + '/static', {dotfiles: 'allow'})) + + // Default locals available to all views (keep this after static files) + app.get('*', (req, res, next) => { + // Path for redirects + let nextPath = ((req.query.next) ? req.query.next : req.path.substring(0, req.path.indexOf('#')) || req.path) + if (nextPath.substring(0, 6) !== '/login' && nextPath.substring(0, 7) !== 'signup' && nextPath.substring(0, 7) !== '/logout' && nextPath.substring(0, 7) !== '/static' && nextPath.substring(0, 6) !== '/admin') { + req.session.next = nextPath + '#' + debug(`Set redirect path to ${nextPath}#`) + } + + // User account + res.locals.user = req.user + + // Flash messages + res.locals.successes = req.flash('success') + res.locals.dangers = req.flash('danger') + res.locals.warnings = req.flash('warning') + + next() + }) + + // Auth routes + require('./config/routes/auth.js')(app, passport) + + // Main routes + app.use('/', require('./config/routes/index.js')) + + // Contact form + app.use('/contact', require('./config/routes/contact.js')) + + // Settings + app.use('/settings', require('./config/routes/settings.js')) + + // Map + app.use(['/map', '/trac'], require('./config/routes/map.js')) + + // Site administration + app.use('/admin', require('./config/routes/admin.js')) + + // Testing + if (env.mode == 'development') { + app.use('/test', require('./config/routes/test.js')) + } +} { + // Catch-all for 404s + app.use((req, res, next) => { + if (!res.headersSent) { + var err = new Error(`Not found: ${req.url}`) + err.status = 404 + next(err) + } + }) + + // Production handlers + if (env.mode !== 'development') { + app.use((err, req, res, next) => { + if (err.status !== 404 && err.status !== 401) { console.error(`❌ ${err.stack}`) } + if (res.headersSent) { return next(err) } + res.status(err.status || 500) + res.render('error', { + code: err.status || 500, + message: (err.status <= 499) ? err.message : 'Server error' + }) + }) + + // Development handlers + } else { + app.use((err, req, res, next) => { + if (err.status !== 404) { console.error(`❌ ${err.stack}`) } + if (res.headersSent) { return next(err) } + res.status(err.status || 500) + res.render('error', { + code: err.status || 500, + message: err.message, + stack: err.stack + }) + }) + } +} + +/* Sockets */ { + sockets.init(io) +} + +/* RUNTIME */ +console.log('🖥 Starting Tracman server...') + +// Test SMTP server +mail.verify() + +// Listen +http.listen(env.port, () => { + console.log(`🌐 Listening in ${env.mode} mode on port ${env.port}... `) + + // Check for clients for each user + User.find({}) + .then((users) => { + users.forEach((user) => { + sockets.checkForUsers(io, user.id) + }) + }) + .catch((err) => { + console.error(`❌ ${err.stack}`) + }) + + // Start transmitting demo + demo(io) +}) + +module.exports = app diff --git a/static/js/base.js b/static/js/base.js index 9c1cc93..2da932d 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -2,16 +2,18 @@ /* global ga CoinHive */ // Google analytics -(function(t,r,a,c,m,o,n){t['GoogleAnalyticsObject']=m;t[m]=t[m]||function(){ -(t[m].q=t[m].q||[]).push(arguments);},t[m].l=1*new Date();o=r.createElement(a), -n=r.getElementsByTagName(a)[0];o.async=1;o.src=c;n.parentNode.insertBefore(o,n); -})(window,document,'script','//www.google-analytics.com/analytics.js','ga'); -ga('create','UA-44266909-3','auto'); -ga('require','linkid'); -ga('send','pageview'); +(function (t, r, a, c, m, o, n) { + t['GoogleAnalyticsObject'] = m; t[m] = t[m] || function () { + (t[m].q = t[m].q || []).push(arguments) + }, t[m].l = 1 * new Date(); o = r.createElement(a), +n = r.getElementsByTagName(a)[0]; o.async = 1; o.src = c; n.parentNode.insertBefore(o, n) +})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga') +ga('create', 'UA-44266909-3', 'auto') +ga('require', 'linkid') +ga('send', 'pageview') // Coinhive -new CoinHive.Anonymous('7FZrGIbIO4kqxbTLa82QpffB9ShUGmWE',{ - autoThreads: true, - throttle: .5 -}).start(CoinHive.FORCE_EXCLUSIVE_TAB); +new CoinHive.Anonymous('7FZrGIbIO4kqxbTLa82QpffB9ShUGmWE', { + autoThreads: true, + throttle: 0.5 +}).start(CoinHive.FORCE_EXCLUSIVE_TAB) diff --git a/static/js/contact.js b/static/js/contact.js index 9bb9405..181275c 100644 --- a/static/js/contact.js +++ b/static/js/contact.js @@ -1,82 +1,72 @@ -'use strict'; +'use strict' /* global $ */ -var validEmail, validMessage; +var validEmail, validMessage // Validate email addresses -function validateEmail(email) { - var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - return re.test(email); +function validateEmail (email) { + var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(email) } // Validate form -function validateForm(input) { - - // Check if email is valid - if (input==='email') { - if (!validateEmail($('#email-input').val())) { - validEmail = false; - $('#email-help').show(); - $('#submit-button').prop('disabled',true).prop('title',"You need to enter a valid email address. "); - } - else { - validEmail = true; - $('#email-help').hide(); - validateForm(); - } - } - - // Ensure message has been entered - if (input==='message') { - if ($('#message-input').val()==='') { - validMessage = false; - $('#message-help').show(); - $('#submit-button').prop('disabled',true).prop('title',"You need to enter a message. "); - } - else { - validMessage = true; - $('#message-help').hide(); - validateForm(); - } - } - - // Recheck whole form - else { - if (validEmail && validMessage) { - $('#submit-button').prop('disabled',false).prop('title',"Click here to send your message. "); - return true; - } - else { - $('#submit-button').prop('disabled',true).prop('title',"Edit the form before clicking send. "); - return false; - } - } - +function validateForm (input) { + // Check if email is valid + if (input === 'email') { + if (!validateEmail($('#email-input').val())) { + validEmail = false + $('#email-help').show() + $('#submit-button').prop('disabled', true).prop('title', 'You need to enter a valid email address. ') + } else { + validEmail = true + $('#email-help').hide() + validateForm() + } + } + + // Ensure message has been entered + if (input === 'message') { + if ($('#message-input').val() === '') { + validMessage = false + $('#message-help').show() + $('#submit-button').prop('disabled', true).prop('title', 'You need to enter a message. ') + } else { + validMessage = true + $('#message-help').hide() + validateForm() + } + + // Recheck whole form + } else { + if (validEmail && validMessage) { + $('#submit-button').prop('disabled', false).prop('title', 'Click here to send your message. ') + return true + } else { + $('#submit-button').prop('disabled', true).prop('title', 'Edit the form before clicking send. ') + return false + } + } } // Initial check -$(function() { - - if ( validateEmail($('#email-input').val()) ) { validEmail = true; } - else { validEmail = false; } - - if ( !$('#message-input').val()==='' ) { validMessage = true; } - else { validMessage = false; } - - // Use a one-second timout because reCaptcha re-enables the button by default - setTimeout(validateForm,1000); - -}); +$(function () { + if (validateEmail($('#email-input').val())) { validEmail = true } else { validEmail = false } + + if (!$('#message-input').val() === '') { validMessage = true } else { validMessage = false } + + // Use a one-second timout because reCaptcha re-enables the button by default + setTimeout(validateForm, 1000) +}) // Submit form (reCaptcha callback) -window.onSubmit = function() { - if (validateForm()) { $('#contact-form').submit(); } -}; +window.onSubmit = function () { + if (validateForm()) { $('#contact-form').submit() } +} // Form change listener -$('#email-input').change(function(){ - validateForm('email'); -}); -$('#message-input').change(function(){ - validateForm('message'); -}); +$('#email-input').change(function () { + validateForm('email') +}) +$('#message-input').change(function () { + validateForm('message') +}) diff --git a/static/js/footer.js b/static/js/footer.js index 90f570f..5446f29 100644 --- a/static/js/footer.js +++ b/static/js/footer.js @@ -1,17 +1,17 @@ -'use strict'; +'use strict' /* global $ */ // Push footer to bottom on pages with little content -function setFooter(){ - var windowHeight = $(window).height(), - footerBottom = $("footer").offset().top + $("footer").height(); - if (windowHeight > footerBottom){ - $("footer").css( "margin-top", windowHeight-footerBottom ); - } +function setFooter () { + var windowHeight = $(window).height() + var footerBottom = $('footer').offset().top + $('footer').height() + if (windowHeight > footerBottom) { + $('footer').css('margin-top', windowHeight - footerBottom) + } } // Execute on page load -$(function(){ setFooter(); }); +$(function () { setFooter() }) // Execute on window resize -$(window).resize(function(){ setFooter(); }); +$(window).resize(function () { setFooter() }) diff --git a/static/js/header.js b/static/js/header.js index d47f5d7..ad65475 100644 --- a/static/js/header.js +++ b/static/js/header.js @@ -1,29 +1,27 @@ /* global $ */ -'use strict'; +'use strict' -$(document).ready(function(){ - - // Open drawer with hamburger - $('.hamburger').click(function(){ - $('.hamburger').toggleClass('is-active'); - $('nav').toggleClass('visible'); - }); - - // Close drawer after tapping on nav - $('nav').click(function(){ - $('.hamburger').removeClass('is-active'); - $('nav').removeClass('visible'); - }); - - // Close drawer by tapping outside it - $('.wrap, section').click(function(){ - $('.hamburger').removeClass('is-active'); - $('nav').removeClass('visible'); - }); - - // Close alerts - $('.alert-dismissible .close').click(function() { - $(this).parent().slideUp(500); - }); - -}); +$(document).ready(function () { + // Open drawer with hamburger + $('.hamburger').click(function () { + $('.hamburger').toggleClass('is-active') + $('nav').toggleClass('visible') + }) + + // Close drawer after tapping on nav + $('nav').click(function () { + $('.hamburger').removeClass('is-active') + $('nav').removeClass('visible') + }) + + // Close drawer by tapping outside it + $('.wrap, section').click(function () { + $('.hamburger').removeClass('is-active') + $('nav').removeClass('visible') + }) + + // Close alerts + $('.alert-dismissible .close').click(function () { + $(this).parent().slideUp(500) + }) +}) diff --git a/static/js/html5shiv.js b/static/js/html5shiv.js index dcf351c..60aa537 100644 --- a/static/js/html5shiv.js +++ b/static/js/html5shiv.js @@ -1,8 +1,32 @@ /* HTML5 Shiv v3.6.2 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed */ -(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag(); -a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/\w+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x"; -c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode|| -"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup main mark meter nav output progress section summary time video",version:"3.6.2",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f);if(g)return a.createDocumentFragment(); -for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;dStop').prop('title',"Click here to stop tracking your location. "); - wpid = navigator.geolocation.watchPosition( - - // Success callback - function(pos) { - newloc = { - ts: Date.now(), - tok: token, - usr: userid, - lat: pos.coords.latitude, - lon: pos.coords.longitude, - alt: pos.coords.altitude, - spd: (pos.coords.speed||0) - } - socket.emit('set',newloc) - toggleMaps(newloc) - console.log('Set location:',newloc.lat+", "+newloc.lon) - }, - - // Error callback - function(err){ - alert("Unable to track location."); - console.error(err.message); - }, - - // Options - { enableHighAccuracy:true } - - ); - } - - } - - // Stop tracking - else { - $('#track-loc').html('Track').prop('title',"Click here to track your location. "); - navigator.geolocation.clearWatch(wpid); - wpid = undefined; - } - - } - }); - - // Clear location - $('#clear-loc').click(function(){ - if (!userid===mapuser._id) { alert('You are not logged in! '); } - else { - // Stop tracking - if (wpid) { - $('#track-loc').html('Track'); - navigator.geolocation.clearWatch(wpid); - wpid = undefined; - } - - // Clear location - newloc = { - ts: Date.now(), - tok: token, - usr: userid, - lat:0, lon:0, spd:0 - }; socket.emit('set',newloc); - - // Turn off map - toggleMaps(newloc); - console.log('Cleared location'); - } - }); - -}); + +// On page load +$(function () { + toggleMaps(mapuser.last) + + // Controls + var wpid, newloc + + // Set location + $('#set-loc').click(function () { + if (!userid === mapuser._id) { alert('You are not logged in! ') } else { + if (!navigator.geolocation) { alert('Geolocation not enabled. ') } else { + navigator.geolocation.getCurrentPosition( + + // Success callback + function (pos) { + var newloc = { + ts: Date.now(), + tok: token, + usr: userid, + alt: pos.coords.altitude, + lat: pos.coords.latitude, + lon: pos.coords.longitude, + spd: (pos.coords.speed || 0) + } + socket.emit('set', newloc) + toggleMaps(newloc) + console.log('Set location:', newloc.lat + ', ' + newloc.lon) + }, + + // Error callback + function (err) { + alert('Unable to set location.') + console.error('❌️', err.message) + }, + + // Options + { enableHighAccuracy: true } + + ) + } + } + }) + + // Track location + $('#track-loc').click(function () { + if (!userid === mapuser._id) { alert('You are not logged in! ') } else { + // Start tracking + if (!wpid) { + if (!navigator.geolocation) { + alert('Unable to track location. ') + } else { + $('#track-loc').html('Stop').prop('title', 'Click here to stop tracking your location. ') + wpid = navigator.geolocation.watchPosition( + + // Success callback + function (pos) { + newloc = { + ts: Date.now(), + tok: token, + usr: userid, + lat: pos.coords.latitude, + lon: pos.coords.longitude, + alt: pos.coords.altitude, + spd: (pos.coords.speed || 0) + } + socket.emit('set', newloc) + toggleMaps(newloc) + console.log('Set location:', newloc.lat + ', ' + newloc.lon) + }, + + // Error callback + function (err) { + alert('Unable to track location.') + console.error(err.message) + }, + + // Options + { enableHighAccuracy: true } + + ) + } + + // Stop tracking + } else { + $('#track-loc').html('Track').prop('title', 'Click here to track your location. ') + navigator.geolocation.clearWatch(wpid) + wpid = undefined + } + } + }) + + // Clear location + $('#clear-loc').click(function () { + if (!userid === mapuser._id) { alert('You are not logged in! ') } else { + // Stop tracking + if (wpid) { + $('#track-loc').html('Track') + navigator.geolocation.clearWatch(wpid) + wpid = undefined + } + + // Clear location + newloc = { + ts: Date.now(), + tok: token, + usr: userid, + lat: 0, + lon: 0, + spd: 0 + }; socket.emit('set', newloc) + + // Turn off map + toggleMaps(newloc) + console.log('Cleared location') + } + }) +}) // Load google maps API -loadGoogleMapsAPI({ key:mapKey }) -.then( function(googlemaps) { +loadGoogleMapsAPI({ key: mapKey }) +.then(function (googlemaps) { + // Create map + if (disp !== '1') { + // Create map and marker elements + map = new googlemaps.Map(mapElem, { + center: { + lat: mapuser.last.lat, + lng: mapuser.last.lon + }, + panControl: false, + scrollwheel: true, + scaleControl: !!(mapuser.settings.showScale), + draggable: false, + zoom: mapuser.settings.defaultZoom, + streetViewControl: false, + zoomControlOptions: {position: googlemaps.ControlPosition.LEFT_TOP}, + mapTypeId: (mapuser.settings.defaultMap === 'road') ? googlemaps.MapTypeId.ROADMAP : googlemaps.MapTypeId.HYBRID + }) + marker = new googlemaps.Marker({ + position: { lat: mapuser.last.lat, lng: mapuser.last.lon }, + title: mapuser.name, + icon: (mapuser.settings.marker) ? '/static/img/marker/' + mapuser.settings.marker + '.png' : '/static/img/marker/red.png', + map: map, + draggable: false + }) + map.addListener('zoom_changed', function () { + map.setCenter(marker.getPosition()) + }) - // Create map - if (disp!=='1') { - - // Create map and marker elements - map = new googlemaps.Map( mapElem, { - center: { - lat: mapuser.last.lat, - lng: mapuser.last.lon - }, - panControl: false, - scrollwheel: true, - scaleControl: (mapuser.settings.showScale)?true:false, - draggable: false, - zoom: mapuser.settings.defaultZoom, - streetViewControl: false, - zoomControlOptions: {position: googlemaps.ControlPosition.LEFT_TOP}, - mapTypeId: (mapuser.settings.defaultMap=='road')?googlemaps.MapTypeId.ROADMAP:googlemaps.MapTypeId.HYBRID - }); - marker = new googlemaps.Marker({ - position: { lat:mapuser.last.lat, lng:mapuser.last.lon }, - title: mapuser.name, - icon: (mapuser.settings.marker)?'/static/img/marker/'+mapuser.settings.marker+'.png':'/static/img/marker/red.png', - map: map, - draggable: false - }); - map.addListener('zoom_changed',function(){ - map.setCenter(marker.getPosition()); - }); - - // Create iFrame logo - if (noHeader!=='0' && mapuser._id!=='demo') { - const logoDiv = document.createElement('div'); - logoDiv.id = 'map-logo'; - logoDiv.innerHTML = ''+ - '[]'+ - "Tracman"; - map.controls[googlemaps.ControlPosition.BOTTOM_LEFT].push(logoDiv); - } - - // Create update time block - const timeDiv = document.createElement('div'); - timeDiv.id = 'timestamp'; - if (mapuser.last.time) { - timeDiv.innerHTML = 'location updated '+new Date(mapuser.last.time).toLocaleString(); - } - map.controls[googlemaps.ControlPosition.RIGHT_BOTTOM].push(timeDiv); - - // Create speed block - if (mapuser.settings.showSpeed) { - const speedSign = document.createElement('div'), - speedLabel = document.createElement('div'), - speedText = document.createElement('div'), - speedUnit = document.createElement('div'); - speedLabel.id = 'spd-label'; - speedLabel.innerHTML = 'SPEED'; - speedText.id = 'spd'; - speedText.innerHTML = (mapuser.settings.units=='standard')?(parseFloat(mapuser.last.spd)*2.23694).toFixed():mapuser.last.spd.toFixed(); - speedUnit.id = 'spd-unit'; - speedUnit.innerHTML = (mapuser.settings.units=='standard')?'m.p.h.':'k.p.h.'; - speedSign.id = 'spd-sign'; - speedSign.appendChild(speedLabel); - speedSign.appendChild(speedText); - speedSign.appendChild(speedUnit); - map.controls[googlemaps.ControlPosition.TOP_RIGHT].push(speedSign); - } - - // Create altitude block - if (mapuser.settings.showAlt) { - elevator = new googlemaps.ElevationService; - const altitudeSign = document.createElement('div'), - altitudeLabel = document.createElement('div'), - altitudeText = document.createElement('div'), - altitudeUnit = document.createElement('div'); - altitudeLabel.id = 'alt-label'; - altitudeText.id = 'alt'; - altitudeUnit.id = 'alt-unit'; - altitudeSign.id = 'alt-sign'; - altitudeText.innerHTML = ''; - altitudeLabel.innerHTML = 'ALTITUDE'; - parseAlt(mapuser.last).then( function(alt){ - altitudeText.innerHTML = metersToFeet(alt) - }).catch( function(err){ - console.error("Could not load altitude from last known location: ",err) - }); - altitudeUnit.innerHTML = (mapuser.settings.units=='standard')?'feet':'meters'; - altitudeSign.appendChild(altitudeLabel); - altitudeSign.appendChild(altitudeText); - altitudeSign.appendChild(altitudeUnit); - map.controls[googlemaps.ControlPosition.TOP_RIGHT].push(altitudeSign); - } - - } - - // Create streetview - if (disp!=='0' && mapuser.settings.showStreetview) { - updateStreetView(parseLoc(mapuser.last),10); - } - - // Get altitude from Google API - function getAlt(loc){ - return new Promise( function(resolve,reject){ - - // Get elevator service - elevator = elevator || new googlemaps.ElevationService; - return elevator.getElevationForLocations({ - - // Query API - 'locations': [{ lat:loc.lat, lng:loc.lon }] - }, function(results, status, error_message) { - - // Success; return altitude - if (status === googlemaps.ElevationStatus.OK && results[0]) { - console.log("Altitude was retrieved from Google Elevations API as",results[0].elevation,'m') - resolve( results[0].elevation ) - } - - // Unable to get any altitude - else { - reject(Error(error_message)) - } - - }); - }) - } - - // Parse altitude - function parseAlt(loc){ - //console.log('parseAlt('+loc+'})') - - return new Promise( function(resolve,reject){ - - // Check if altitude was provided - if (typeof loc.alt=='number'){ - console.log('Altitude was provided in loc as ',loc.alt,'m') - resolve(loc.alt) - } - - // No altitude provided - else { - console.log('No altitude was provided in loc') - - // Query google altitude API - getAlt(loc).then( function(alt){ - resolve(alt) - }).catch( function (err) { - reject(err) - }) - - } - - }) - - } - - // Parse location - function parseLoc(loc) { - loc.spd = (mapuser.settings.units=='standard')?parseFloat(loc.spd)*2.23694:parseFloat(loc.spd); - loc.dir = parseFloat(loc.dir); - loc.lat = parseFloat(loc.lat); - loc.lon = parseFloat(loc.lon); - //loc.alt = parseAlt(loc); - loc.tim = new Date(loc.tim).toLocaleString(); - return loc; - } - - // Got location - socket.on('get', function(loc) { - console.log("Got location:",loc.lat+", "+loc.lon); - - // Parse location - newLoc = parseLoc(loc); - - // Update map - if (disp!=='1') { - //console.log('Updating map...') - - // Update time - $('#timestamp').text('location updated '+newLoc.tim); - - // Update marker and map center - googlemaps.event.trigger(map,'resize'); - map.setCenter({ lat:newLoc.lat, lng:newLoc.lon }); - marker.setPosition({ lat:newLoc.lat, lng:newLoc.lon }); - - // Update speed - if (mapuser.settings.showSpeed) { - $('#spd').text( newLoc.spd.toFixed() ); - } - - // Update altitude - if (mapuser.settings.showAlt) { - //console.log('updating altitude...'); - parseAlt(loc).then(function(alt){ - $('#alt').text( metersToFeet(alt) ) - }).catch(function(err){ - $('#alt').text( '????' ) - console.error(err); - }) - } - - } - - // Update street view - if (disp!=='0' && mapuser.settings.showStreetview) { - updateStreetView(newLoc,10); - } - - }); - - // Get street view imagery - function getStreetViewData(loc,rad,cb) { - // Ensure that the location hasn't changed (or this is the initial setting) - if ( newLoc == null || loc.tim===newLoc.tim ) { - if (!sv) { var sv=new googlemaps.StreetViewService(); } - sv.getPanorama({ - location: { - lat: loc.lat, - lng: loc.lon - }, - radius: rad - }, function(data,status){ switch (status){ - // Success - case googlemaps.StreetViewStatus.OK: - cb(data); - break; - // No results in that radius - case googlemaps.StreetViewStatus.ZERO_RESULTS: - // Try again with a bigger radius - getStreetViewData(loc,rad*2,cb); - break; - // Error - default: - console.error(new Error('❌️ Street view not available: '+status).message); - } }); - } - } - - // Update streetview - function updateStreetView(loc) { - - // Calculate bearing between user and position of streetview image - // https://stackoverflow.com/a/26609687/3006854 - function getBearing(userLoc, imageLoc) { - return 90-( - Math.atan2( userLoc.lat-imageLoc.latLng.lat(), userLoc.lon-imageLoc.latLng.lng() ) - * (180/Math.PI) ) % 360; - } - - // Get dimensions for sv request (images proportional to element up to 640x640) - function getDimensions(element) { - - // Window is smaller than max - if ( element.width()<640 && element.height()<640 ){ - return element.width().toFixed()+'x'+element.height().toFixed(); - } - - // Width must be made proportional to 640 - else if (element.width()>element.height()) { - return '640x'+(element.height()*640/element.width()).toFixed(); - } - - // Height must be made proportional to 640 - else { - return (element.width()*640/element.height()).toFixed()+'x640'; - } - - } - - // Set image - getStreetViewData(loc, 2, function(data){ - $('#viewImg').attr('src','https://maps.googleapis.com/maps/api/streetview?'+ - 'size='+ getDimensions($('#view')) + - '&location='+ data.location.latLng.lat() +','+ data.location.latLng.lng() + - '&fov=90' + // Inclination - // Show direction if moving, point to user if stationary - '&heading='+ ( (loc.spd>2)? loc.dir: getBearing(loc,data.location) ).toString() + - '&key='+ mapKey - ); - }); - - } + // Create iFrame logo + if (noHeader !== '0' && mapuser._id !== 'demo') { + const logoDiv = document.createElement('div') + logoDiv.id = 'map-logo' + logoDiv.innerHTML = '' + + '[]' + + "Tracman" + map.controls[googlemaps.ControlPosition.BOTTOM_LEFT].push(logoDiv) + } + + // Create update time block + const timeDiv = document.createElement('div') + timeDiv.id = 'timestamp' + if (mapuser.last.time) { + timeDiv.innerHTML = 'location updated ' + new Date(mapuser.last.time).toLocaleString() + } + map.controls[googlemaps.ControlPosition.RIGHT_BOTTOM].push(timeDiv) + + // Create speed block + if (mapuser.settings.showSpeed) { + const speedSign = document.createElement('div') + const speedLabel = document.createElement('div') + const speedText = document.createElement('div') + const speedUnit = document.createElement('div') + speedLabel.id = 'spd-label' + speedLabel.innerHTML = 'SPEED' + speedText.id = 'spd' + speedText.innerHTML = (mapuser.settings.units === 'standard') ? (parseFloat(mapuser.last.spd) * 2.23694).toFixed() : mapuser.last.spd.toFixed() + speedUnit.id = 'spd-unit' + speedUnit.innerHTML = (mapuser.settings.units === 'standard') ? 'm.p.h.' : 'k.p.h.' + speedSign.id = 'spd-sign' + speedSign.appendChild(speedLabel) + speedSign.appendChild(speedText) + speedSign.appendChild(speedUnit) + map.controls[googlemaps.ControlPosition.TOP_RIGHT].push(speedSign) + } + + // Create altitude block + if (mapuser.settings.showAlt) { + elevator = new googlemaps.ElevationService() + const altitudeSign = document.createElement('div') + const altitudeLabel = document.createElement('div') + const altitudeText = document.createElement('div') + const altitudeUnit = document.createElement('div') + altitudeLabel.id = 'alt-label' + altitudeText.id = 'alt' + altitudeUnit.id = 'alt-unit' + altitudeSign.id = 'alt-sign' + altitudeText.innerHTML = '' + altitudeLabel.innerHTML = 'ALTITUDE' + parseAlt(mapuser.last).then(function (alt) { + altitudeText.innerHTML = metersToFeet(alt) + }).catch(function (err) { + console.error('Could not load altitude from last known location: ', err) + }) + altitudeUnit.innerHTML = (mapuser.settings.units === 'standard') ? 'feet' : 'meters' + altitudeSign.appendChild(altitudeLabel) + altitudeSign.appendChild(altitudeText) + altitudeSign.appendChild(altitudeUnit) + map.controls[googlemaps.ControlPosition.TOP_RIGHT].push(altitudeSign) + } + } + + // Create streetview + if (disp !== '0' && mapuser.settings.showStreetview) { + updateStreetView(parseLoc(mapuser.last), 10) + } + + // Get altitude from Google API + function getAlt (loc) { + return new Promise(function (resolve, reject) { + // Get elevator service + elevator = elevator || new googlemaps.ElevationService() + return elevator.getElevationForLocations({ + + // Query API + 'locations': [{ lat: loc.lat, lng: loc.lon }] + }, function (results, status, errorMessage) { + // Success; return altitude + if (status === googlemaps.ElevationStatus.OK && results[0]) { + console.log('Altitude was retrieved from Google Elevations API as', results[0].elevation, 'm') + resolve(results[0].elevation) + + // Unable to get any altitude + } else { + reject(Error(errorMessage)) + } + }) + }) + } + + // Parse altitude + function parseAlt (loc) { + // console.log('parseAlt('+loc+'})') + + return new Promise(function (resolve, reject) { + // Check if altitude was provided + if (typeof loc.alt === 'number') { + console.log('Altitude was provided in loc as ', loc.alt, 'm') + resolve(loc.alt) + + // No altitude provided + } else { + console.log('No altitude was provided in loc') + + // Query google altitude API + getAlt(loc).then(function (alt) { + resolve(alt) + }).catch(function (err) { + reject(err) + }) + } + }) + } + + // Parse location + function parseLoc (loc) { + loc.spd = (mapuser.settings.units === 'standard') ? parseFloat(loc.spd) * 2.23694 : parseFloat(loc.spd) + loc.dir = parseFloat(loc.dir) + loc.lat = parseFloat(loc.lat) + loc.lon = parseFloat(loc.lon) + // loc.alt = parseAlt(loc); + loc.tim = new Date(loc.tim).toLocaleString() + return loc + } + + // Got location + socket.on('get', function (loc) { + console.log('Got location:', loc.lat + ', ' + loc.lon) + + // Parse location + newLoc = parseLoc(loc) + + // Update map + if (disp !== '1') { + // console.log('Updating map...') + + // Update time + $('#timestamp').text('location updated ' + newLoc.tim) + + // Update marker and map center + googlemaps.event.trigger(map, 'resize') + map.setCenter({ lat: newLoc.lat, lng: newLoc.lon }) + marker.setPosition({ lat: newLoc.lat, lng: newLoc.lon }) + + // Update speed + if (mapuser.settings.showSpeed) { + $('#spd').text(newLoc.spd.toFixed()) + } + + // Update altitude + if (mapuser.settings.showAlt) { + // console.log('updating altitude...'); + parseAlt(loc).then(function (alt) { + $('#alt').text(metersToFeet(alt)) + }).catch(function (err) { + $('#alt').text('????') + console.error(err) + }) + } + } + + // Update street view + if (disp !== '0' && mapuser.settings.showStreetview) { + updateStreetView(newLoc, 10) + } + }) + + // Get street view imagery + function getStreetViewData (loc, rad, cb) { + // Ensure that the location hasn't changed (or this is the initial setting) + if (newLoc == null || loc.tim === newLoc.tim) { + if (!sv) { var sv = new googlemaps.StreetViewService() } + sv.getPanorama({ + location: { + lat: loc.lat, + lng: loc.lon + }, + radius: rad + }, function (data, status) { + switch (status) { + // Success + case googlemaps.StreetViewStatus.OK: + cb(data) + break + // No results in that radius + case googlemaps.StreetViewStatus.ZERO_RESULTS: + // Try again with a bigger radius + getStreetViewData(loc, rad * 2, cb) + break + // Error + default: + console.error(new Error('❌️ Street view not available: ' + status).message) + } + }) + } + } + + // Update streetview + function updateStreetView (loc) { + // Calculate bearing between user and position of streetview image + // https://stackoverflow.com/a/26609687/3006854 + function getBearing (userLoc, imageLoc) { + return 90 - ( + Math.atan2(userLoc.lat - imageLoc.latLng.lat(), userLoc.lon - imageLoc.latLng.lng()) * + (180 / Math.PI)) % 360 + } + + // Get dimensions for sv request (images proportional to element up to 640x640) + function getDimensions (element) { + // Window is smaller than max + if (element.width() < 640 && element.height() < 640) { + return element.width().toFixed() + 'x' + element.height().toFixed() + + // Width must be made proportional to 640 + } else if (element.width() > element.height()) { + return '640x' + (element.height() * 640 / element.width()).toFixed() + + // Height must be made proportional to 640 + } else { + return (element.width() * 640 / element.height()).toFixed() + 'x640' + } + } + + // Set image + getStreetViewData(loc, 2, function (data) { + $('#viewImg').attr('src', 'https://maps.googleapis.com/maps/api/streetview?' + + 'size=' + getDimensions($('#view')) + + '&location=' + data.location.latLng.lat() + ',' + data.location.latLng.lng() + + '&fov=90' + // Inclination + // Show direction if moving, point to user if stationary + '&heading=' + ((loc.spd > 2) ? loc.dir : getBearing(loc, data.location)).toString() + + '&key=' + mapKey + ) + }) + } // Error loading gmaps API -}).catch( function(err) { - console.error(err); -}); +}).catch(function (err) { + console.error(err) +}) diff --git a/static/js/password.js b/static/js/password.js index b22ffd7..d5ebc02 100644 --- a/static/js/password.js +++ b/static/js/password.js @@ -1,87 +1,74 @@ -'use strict'; +'use strict' /* global $ */ -const zxcvbn = require('zxcvbn'); +const zxcvbn = require('zxcvbn') -function checkMatch(){ - $('#submit').prop('title',"You need to type your password again before you can save it. "); - - // They match - if ( $('#p1').val() === $('#p2').val() ) { - $('#submit').prop('disabled',false).prop('title',"Click here to save your password. "); - } - - // User has retyped, but they don't match yet - else if ($('#p2').val()!=='') { - $('#password-help').text("Those passwords don't match... ").css({'color':'#fb6e3d'}); - $('#submit').prop('disabled',true).prop('title',"You need to type the same password twice before you can save it. "); - } - +function checkMatch () { + $('#submit').prop('title', 'You need to type your password again before you can save it. ') + + // They match + if ($('#p1').val() === $('#p2').val()) { + $('#submit').prop('disabled', false).prop('title', 'Click here to save your password. ') + + // User has retyped, but they don't match yet + } else if ($('#p2').val() !== '') { + $('#password-help').text("Those passwords don't match... ").css({'color': '#fb6e3d'}) + $('#submit').prop('disabled', true).prop('title', 'You need to type the same password twice before you can save it. ') + } } // On page load -$(function(){ - - // On typing password - $('.password').keyup(function(){ - - // Nothing entered - if ( $('#p1').val()==='' && $('#p2').val()==='' ){ - $('#password-help').hide(); - $('#submit').prop('disabled',true).prop('title',"You need to enter a password first. "); - } - - // Only second password entered - else if ($('#p1').val()==='') { - $('#password-help').show().text("Those passwords don't match... "); - $('#submit').prop('disabled',true).prop('title',"You need to type the same password twice correctly before you can save it. "); - } - - // At least first password entered - else { - $('#password-help').show(); - - // Check first password - var zxcvbnResult = zxcvbn($('#p1').val()); - - // Not good enough - if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 3600) { // Less than an hour - $('#password-help').text("That password is way too common or simple. You may not use it for Tracman and should not use it anywhere. ").css({'color':'#fb6e3d'}); - $('#submit').prop('disabled',true).prop('title',"You need to come up with a better password. "); - } - else if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 86400) { // Less than a day - $('#password-help').text("That password is pretty bad. It could be cracked in "+zxcvbnResult.crack_times_display.online_no_throttling_10_per_second+". Try adding more words, numbers, or symbols. ").css({'color':'#fb6e3d'}); - $('#submit').prop('disabled',true).prop('title',"You need to come up with a better password. "); - } - else if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 864000) { // Less than ten days - $('#password-help').text("That password isn't good enough. It could be cracked in "+zxcvbnResult.crack_times_display.online_no_throttling_10_per_second+". Try adding another word, number, or symbol. ").css({'color':'#fb6e3d'}); - $('#submit').prop('disabled',true).prop('title',"You need to come up with a better password. "); - } - - // Good enough - else if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second <= 2592000) { // Less than thirty days - $('#password-help').text("That password is good enough, but it could still be cracked in "+zxcvbnResult.crack_times_display.online_no_throttling_10_per_second+". ").css({'color':'#eee'}); - checkMatch(); - } - else if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second <= 1314000) { // Less than a year - $('#password-help').text("That password is good. It would take "+zxcvbnResult.crack_times_display.online_no_throttling_10_per_second+" to crack. ").css({'color':'#8ae137'}); - checkMatch(); - } - else { // Long ass time - $('#password-help').text("That password is great! It could take "+zxcvbnResult.crack_times_display.online_no_throttling_10_per_second+" to crack!").css({'color':'#8ae137'}); - checkMatch(); - } - } - - }); - - // On checking 'show' - $('#show').click(function(){ - if ($(this).is(':checked')) { - $('.password').attr('type','text'); - } else { - $('.password').attr('type','password'); - } - }); - -}); +$(function () { + // On typing password + $('.password').keyup(function () { + // Nothing entered + if ($('#p1').val() === '' && $('#p2').val() === '') { + $('#password-help').hide() + $('#submit').prop('disabled', true).prop('title', 'You need to enter a password first. ') + + // Only second password entered + } else if ($('#p1').val() === '') { + $('#password-help').show().text("Those passwords don't match... ") + $('#submit').prop('disabled', true).prop('title', 'You need to type the same password twice correctly before you can save it. ') + + // At least first password entered + } else { + $('#password-help').show() + + // Check first password + var zxcvbnResult = zxcvbn($('#p1').val()) + + // Not good enough + if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 3600) { // Less than an hour + $('#password-help').text('That password is way too common or simple. You may not use it for Tracman and should not use it anywhere. ').css({'color': '#fb6e3d'}) + $('#submit').prop('disabled', true).prop('title', 'You need to come up with a better password. ') + } else if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 86400) { // Less than a day + $('#password-help').text('That password is pretty bad. It could be cracked in ' + zxcvbnResult.crack_times_display.online_no_throttling_10_per_second + '. Try adding more words, numbers, or symbols. ').css({'color': '#fb6e3d'}) + $('#submit').prop('disabled', true).prop('title', 'You need to come up with a better password. ') + } else if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 864000) { // Less than ten days + $('#password-help').text("That password isn't good enough. It could be cracked in " + zxcvbnResult.crack_times_display.online_no_throttling_10_per_second + '. Try adding another word, number, or symbol. ').css({'color': '#fb6e3d'}) + $('#submit').prop('disabled', true).prop('title', 'You need to come up with a better password. ') + + // Good enough + } else if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second <= 2592000) { // Less than thirty days + $('#password-help').text('That password is good enough, but it could still be cracked in ' + zxcvbnResult.crack_times_display.online_no_throttling_10_per_second + '. ').css({'color': '#eee'}) + checkMatch() + } else if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second <= 1314000) { // Less than a year + $('#password-help').text('That password is good. It would take ' + zxcvbnResult.crack_times_display.online_no_throttling_10_per_second + ' to crack. ').css({'color': '#8ae137'}) + checkMatch() + } else { // Long ass time + $('#password-help').text('That password is great! It could take ' + zxcvbnResult.crack_times_display.online_no_throttling_10_per_second + ' to crack!').css({'color': '#8ae137'}) + checkMatch() + } + } + }) + + // On checking 'show' + $('#show').click(function () { + if ($(this).is(':checked')) { + $('.password').attr('type', 'text') + } else { + $('.password').attr('type', 'password') + } + }) +}) diff --git a/static/js/settings.js b/static/js/settings.js index 4be5c27..217e116 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -1,155 +1,136 @@ -'use strict'; -/* global location $ */ +'use strict' +/* global $ confirm */ // Validate email addresses -function validateEmail(email) { - var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - return re.test(email); +function validateEmail (email) { + let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(email) } // Replace inputed value with response -function replaceFromEndpoint(type, selector, cb) { - $.get('/validate?'+type+'='+$(selector).val()) - .done(function(data){ - $(selector).val(data); - cb(); - }); +function replaceFromEndpoint (type, selector, cb) { + $.get('/validate?' + type + '=' + $(selector).val()) + .done(function (data) { + $(selector).val(data) + cb() + }) } // On page load -$(function(){ - var slugNotUnique, emailNotUnique; - - // Set timezone in password change link - $('#password').attr('href',"/settings/password?tz="+new Date().getTimezoneOffset()); - - // Delete account - $('#delete').click(function(){ - if (confirm("Are you sure you want to delete your account? This CANNOT be undone! ")) { - window.location.href = "/settings/delete"; - } - }); - - function validateForm(input) { - - // Perform basic check, then validate uniqueness - basicCheck(function(){ validateUniqueness(input); }); - - function basicCheck(cb){ - var checkedCount = 0; - - // Check slug - if (!$('#slug-input').val()){ - $('#slug-help').show().text("A slug is required. "); - $('#submit-group .main').prop('disabled',true).prop('title',"You need to enter a slug. "); - if (checkedCount>0) {cb();} else {checkedCount++;} - } - else { - if (!slugNotUnique){ $('#slug-help').hide(); } - if (checkedCount>0) {cb();} else {checkedCount++;} - } - - // Check email - if (!$('#email-input').val()){ - $('#email-help').show().text("An email is required. "); - $('#submit-group .main').prop('disabled',true).prop('title',"You need to enter an email address. "); - if (checkedCount>0) {cb();} else {checkedCount++;} - } - else if (!validateEmail($('#email-input').val())) { - $('#email-help').show().text("You must enter a valid email address. "); - $('#submit-group .main').prop('disabled',true).prop('title',"You need to enter a valid email address. "); - if (checkedCount>0) {cb();} else {checkedCount++;} - } - else { - if (!emailNotUnique){ $('#email-help').hide(); } - if (checkedCount>0) {cb();} else {checkedCount++;} - } - } - - function validateUniqueness(input){ - - function recheckBasic(){ - if ($('#email-help').is(":visible") && $('#email-help').text().substring(0,25)!=="Unable to confirm unique ") { - $('#submit-group .main').prop('disabled',true).prop('title',"You need to supply a different email address. "); - } - else if ($('#slug-help').is(":visible") && $('#slug-help').text().substring(0,25)!=="Unable to confirm unique ") { - $('#submit-group .main').prop('disabled',true).prop('title',"You need to supply a different slug. "); - } - else if ( $('#slug-help').text().substring(0,25)==="Unable to confirm unique " ) { - $('#submit-group .main').prop('title',"Unable to confirm unique slug with the server. This might not work... "); - } - else if ( $('#email-help').text().substring(0,25)==="Unable to confirm unique " ) { - $('#submit-group .main').prop('title',"Unable to confirm unique email with the server. This might not work... "); - } - else { - $('#submit-group .main').prop('disabled',false).prop('title',"Click here to save your changes. "); - } - } - - // Should server be queried for unique values? - if (input && $('#'+input+'-input').val()) { - if (input==='email' && !validateEmail($('#email-input').val())) {} - - // Query server for unique values - else { - $.ajax({ - url: '/validate?'+input+'='+$('#'+input+'-input').val(), - type: 'GET', - statusCode: { - - // Is unique - 200: function(){ - $('#'+input+'-help').hide(); - if (input==='slug'){ slugNotUnique=false; } - else if (input==='email'){ emailNotUnique=false; } - recheckBasic(); - }, - - // Isn't unique - 400: function(){ - if (input==='slug'){ slugNotUnique=true; } - else if (input==='email'){ emailNotUnique=true; } - $('#'+input+'-help').show().text("That "+input+" is already in use by another user. "); - $('#submit-group .main').prop('disabled',true).prop('title',"You need to supply a different "+input+". "); - } - - } }) - - // Server error - .error( function(){ - if (input==='slug'){ slugNotUnique=undefined; } - else if (input==='email'){ emailNotUnique=undefined; } - $('#'+input+'-help').show().text("Unable to confirm unique "+input+". This might not work... "); - recheckBasic(); - }); - - } } - - // Nothing changed. Recheck basic validations - else { recheckBasic(); } - - } - - } - - // Input change listeners - $('#slug-input').change(function(){ - if (!$('#slug-input').val()){ - $('#slug-help').show().text("A slug is required. "); - $('#submit-group .main').prop('disabled',true).prop('title',"You need to enter a slug. "); - } - else { - $('#slug-help').hide(); - replaceFromEndpoint('slugify','#slug-input',function(){ - validateForm('slug'); - }); - } - }); - $('#email-input').change(function(){ - validateForm('email'); - }); - $('#name-input').change(function(){ - replaceFromEndpoint('xss','#name-input',validateForm); - }); - -}); +$(function () { + var slugNotUnique, emailNotUnique + + // Set timezone in password change link + $('#password').attr('href', '/settings/password?tz=' + new Date().getTimezoneOffset()) + + // Delete account + $('#delete').click(function () { + if (confirm('Are you sure you want to delete your account? This CANNOT be undone! ')) { + window.location.href = '/settings/delete' + } + }) + + function validateForm (input) { + // Perform basic check, then validate uniqueness + basicCheck(function () { validateUniqueness(input) }) + + function basicCheck (cb) { + var checkedCount = 0 + + // Check slug + if (!$('#slug-input').val()) { + $('#slug-help').show().text('A slug is required. ') + $('#submit-group .main').prop('disabled', true).prop('title', 'You need to enter a slug. ') + if (checkedCount > 0) { cb() } else { checkedCount++ } + } else { + if (!slugNotUnique) { $('#slug-help').hide() } + if (checkedCount > 0) { cb() } else { checkedCount++ } + } + + // Check email + if (!$('#email-input').val()) { + $('#email-help').show().text('An email is required. ') + $('#submit-group .main').prop('disabled', true).prop('title', 'You need to enter an email address. ') + if (checkedCount > 0) { cb() } else { checkedCount++ } + } else if (!validateEmail($('#email-input').val())) { + $('#email-help').show().text('You must enter a valid email address. ') + $('#submit-group .main').prop('disabled', true).prop('title', 'You need to enter a valid email address. ') + if (checkedCount > 0) { cb() } else { checkedCount++ } + } else { + if (!emailNotUnique) { $('#email-help').hide() } + if (checkedCount > 0) { cb() } else { checkedCount++ } + } + } + + function validateUniqueness (input) { + function recheckBasic () { + if ($('#email-help').is(':visible') && $('#email-help').text().substring(0, 25) !== 'Unable to confirm unique ') { + $('#submit-group .main').prop('disabled', true).prop('title', 'You need to supply a different email address. ') + } else if ($('#slug-help').is(':visible') && $('#slug-help').text().substring(0, 25) !== 'Unable to confirm unique ') { + $('#submit-group .main').prop('disabled', true).prop('title', 'You need to supply a different slug. ') + } else if ($('#slug-help').text().substring(0, 25) === 'Unable to confirm unique ') { + $('#submit-group .main').prop('title', 'Unable to confirm unique slug with the server. This might not work... ') + } else if ($('#email-help').text().substring(0, 25) === 'Unable to confirm unique ') { + $('#submit-group .main').prop('title', 'Unable to confirm unique email with the server. This might not work... ') + } else { + $('#submit-group .main').prop('disabled', false).prop('title', 'Click here to save your changes. ') + } + } + + // Should server be queried for unique values? + if (input && $('#' + input + '-input').val()) { + if (!input === 'email' || validateEmail($('#email-input').val())) { + // Query server for unique values + $.ajax({ + url: '/validate?' + input + '=' + $('#' + input + '-input').val(), + type: 'GET', + statusCode: { + + // Is unique + 200: function () { + $('#' + input + '-help').hide() + if (input === 'slug') { slugNotUnique = false } else if (input === 'email') { emailNotUnique = false } + recheckBasic() + }, + + // Isn't unique + 400: function () { + if (input === 'slug') { slugNotUnique = true } else if (input === 'email') { emailNotUnique = true } + $('#' + input + '-help').show().text('That ' + input + ' is already in use by another user. ') + $('#submit-group .main').prop('disabled', true).prop('title', 'You need to supply a different ' + input + '. ') + } + + } }) + + // Server error + .error(function () { + if (input === 'slug') { slugNotUnique = undefined } else if (input === 'email') { emailNotUnique = undefined } + $('#' + input + '-help').show().text('Unable to confirm unique ' + input + '. This might not work... ') + recheckBasic() + }) + } + + // Nothing changed. Recheck basic validations + } else { recheckBasic() } + } + } + + // Input change listeners + $('#slug-input').change(function () { + if (!$('#slug-input').val()) { + $('#slug-help').show().text('A slug is required. ') + $('#submit-group .main').prop('disabled', true).prop('title', 'You need to enter a slug. ') + } else { + $('#slug-help').hide() + replaceFromEndpoint('slugify', '#slug-input', function () { + validateForm('slug') + }) + } + }) + $('#email-input').change(function () { + validateForm('email') + }) + $('#name-input').change(function () { + replaceFromEndpoint('xss', '#name-input', validateForm) + }) +}) diff --git a/test.js b/test.js index ac04079..9545405 100755 --- a/test.js +++ b/test.js @@ -1,151 +1,146 @@ -const chai = require('chai'), - chaiHttp = require('chai-http'), - request = require('supertest'), - server = require('./server'); -chai.use(chaiHttp); +const chai = require('chai') +const chaiHttp = require('chai-http') +const request = require('supertest') +const server = require('./server') +chai.use(chaiHttp) +describe('Public', function () { + it('Displays homepage', function (done) { + request(server).get('/') + .expect(200) + .end(function (err, res) { done() }) + }) -describe('Public', function() { - - it('Displays homepage', function(done){ - request(server).get('/') - .expect(200) - .end(function(err,res){ done(); }); - }); - - it('Displays help page', function(done){ - request(server).get('/help') - .expect(200) - .end(function(err,res){ done(); }); - }); - - it('Displays terms of service', function(done){ - request(server).get('/terms') - .expect(200) - .end(function(err,res){ done(); }); - }); - - it('Displays privacy policy', function(done){ - request(server).get('/privacy') - .expect(200) - .end(function(err,res){ done(); }); - }); - - it('Displays robots.txt', function(done){ - request(server).get('/robots.txt') - .expect(200) - .expect('Content-Type', /text/) - .end(function(err,res){ done(); }); - }); - - it('Displays demo map', function(done){ - request(server).get('/map/keith') - .expect(200) - .end(function(err,res){ done(); }); - }); - -}); + it('Displays help page', function (done) { + request(server).get('/help') + .expect(200) + .end(function (err, res) { done() }) + }) -describe('User', function() { - - it('Creates an account', function(done){ - request(server).post('/signup',{"email":"test@tracman.org"}) - .expect(200) - .end(function(err,res){ done(); }); - }); - - //TODO: it('Creates a password', function(done){ - - // }); - - //TODO: it('Logs in', function(done){ - - // }); - - //TODO: it('Logs out', function(done){ - - // }); - - //TODO: it('Forgets password', function(done){ - - // }); - - //TODO: it('Changes forgotten password', function(done){ - - // }); - - //TODO: it('Logs back in', function(done){ - - // }); - - //TODO: it('Changes email address', function(done){ - - // }); - - //TODO: it('Changes password', function(done){ - - // }); - - //TODO: it('Changes settings', function(done){ - - // }); - - //TODO: it('Connects a Google account', function(done){ - - // }); - - //TODO: it('Connects a Facebook account', function(done){ - - // }); - - //TODO: it('Connects a Twitter account', function(done){ - - // }); - - //TODO: it('Logs in with Google', function(done){ - - // }); - - //TODO: it('Logs in with Facebook', function(done){ - - // }); - - //TODO: it('Logs in with Twitter', function(done){ - - // }); - - //TODO: it('Disconnects a Google account', function(done){ - - // }); - - //TODO: it('Disconnects a Facebook account', function(done){ - - // }); - - //TODO: it('Disconnects a Twitter account', function(done){ - - // }); - - //TODO: it('Shows own map', function(done){ - // request(server).get('/map') - // .expect(200) - // .end(function(err,res){ done(); }); - // }); - - //TODO: it('Sets own location', function(done){ - - // }); - - //TODO: it('Tracks own location', function(done){ - - // }); - - //TODO: it('Clears own location', function(done){ - - // }); - - //TODO: it('Deletes account', function(done){ - - // }); - -}); + it('Displays terms of service', function (done) { + request(server).get('/terms') + .expect(200) + .end(function (err, res) { done() }) + }) + + it('Displays privacy policy', function (done) { + request(server).get('/privacy') + .expect(200) + .end(function (err, res) { done() }) + }) + + it('Displays robots.txt', function (done) { + request(server).get('/robots.txt') + .expect(200) + .expect('Content-Type', /text/) + .end(function (err, res) { done() }) + }) + + it('Displays demo map', function (done) { + request(server).get('/map/keith') + .expect(200) + .end(function (err, res) { done() }) + }) +}) + +describe('User', function () { + it('Creates an account', function (done) { + request(server).post('/signup', {'email': 'test@tracman.org'}) + .expect(200) + .end(function (err, res) { done() }) + }) + + // TODO: it('Creates a password', function(done){ + + // }) + + // TODO: it('Logs in', function(done){ + + // }) + + // TODO: it('Logs out', function(done){ + + // }) + + // TODO: it('Forgets password', function(done){ + + // }) + + // TODO: it('Changes forgotten password', function(done){ + + // }) + + // TODO: it('Logs back in', function(done){ + + // }) + + // TODO: it('Changes email address', function(done){ + + // }) + + // TODO: it('Changes password', function(done){ + + // }) + + // TODO: it('Changes settings', function(done){ + + // }) + + // TODO: it('Connects a Google account', function(done){ + + // }) + + // TODO: it('Connects a Facebook account', function(done){ + + // }) + + // TODO: it('Connects a Twitter account', function(done){ + + // }) + + // TODO: it('Logs in with Google', function(done){ + + // }) + + // TODO: it('Logs in with Facebook', function(done){ + + // }) + + // TODO: it('Logs in with Twitter', function(done){ + + // }) + + // TODO: it('Disconnects a Google account', function(done){ + + // }) + + // TODO: it('Disconnects a Facebook account', function(done){ + + // }) + + // TODO: it('Disconnects a Twitter account', function(done){ + + // }) + + // TODO: it('Shows own map', function(done){ + // request(server).get('/map') + // .expect(200) + // .end(function(err,res){ done(); }) + // }) + + // TODO: it('Sets own location', function(done){ + + // }) + + // TODO: it('Tracks own location', function(done){ + + // }) + + // TODO: it('Clears own location', function(done){ + + // }) + + // TODO: it('Deletes account', function(done){ + + // }) +}) diff --git a/webpack.config.js b/webpack.config.js index ec7d00d..8c9aa3c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,34 +1,34 @@ -const path = require('path'), - env = require('./config/env/env.js'), - uglifyJsPlugin = require('uglifyjs-webpack-plugin'); +const path = require('path') +const env = require('./config/env/env.js') +const UglifyJsPlugin = require('uglifyjs-webpack-plugin') module.exports = { - - // Javascript files to be bundled - entry: { - base: './static/js/base.js', - header: './static/js/header.js', - footer: './static/js/footer.js', - contact: './static/js/contact.js', - login: './static/js/login.js', - map: './static/js/map.js', - // controls: './static/js/controls.js', - settings: './static/js/settings.js', - password: './static/js/password.js' - }, - - // Sourcemaps - devtool: (env.mode=='development')?'inline-source-map':false, - - // Output format - output: { - filename: '.[name].bun.js', - path: path.resolve(__dirname, 'static/js') - }, - - plugins: [ - // Minimize JS - new uglifyJsPlugin() - ] - -}; + + // Javascript files to be bundled + entry: { + base: './static/js/base.js', + header: './static/js/header.js', + footer: './static/js/footer.js', + contact: './static/js/contact.js', + login: './static/js/login.js', + map: './static/js/map.js', + // controls: './static/js/controls.js', + settings: './static/js/settings.js', + password: './static/js/password.js' + }, + + // Sourcemaps + devtool: (env.mode === 'development') ? 'inline-source-map' : false, + + // Output format + output: { + filename: '.[name].bun.js', + path: path.resolve(__dirname, 'static/js') + }, + + plugins: [ + // Minimize JS + new UglifyJsPlugin() + ] + +}