Added account creation, switched to arrow functions

master
Keith Irwin 2017-04-13 18:53:18 -04:00
parent 3e08a462f2
commit 67061fd99c
No known key found for this signature in database
GPG Key ID: 378933C743E2BBC0
17 changed files with 529 additions and 647 deletions

View File

@ -4,10 +4,12 @@ const
mw = require('./middleware.js'),
mail = require('./mail.js'),
User = require('./models.js').user,
slug = require('slug'),
crypto = require('crypto'),
env = require('./env.js');
module.exports = function(app, passport) {
module.exports = (app, passport) => {
// Methods for success and failure
const
loginOutcome = {
@ -18,130 +20,209 @@ module.exports = function(app, passport) {
failureRedirect: '/settings',
failureFlash: true
},
loginCallback = function(req,res){
loginCallback = (req,res)=>{
res.redirect( req.session.next || '/settings' );
delete req.session.next;
};
// Login/-out
app.route('/login')
.get( function(req,res){
if (req.isAuthenticated()){
res.redirect('/settings'); }
.get( (req,res)=>{
req.session.next = req.header('Referer');
if (req.isAuthenticated()){
res.redirect(req.session.next||'/settings'); }
else { res.render('login'); }
})
.post( passport.authenticate('local',loginOutcome), loginCallback );
app.get('/logout', function(req,res){
app.get('/logout', (req,res)=>{
req.logout();
req.flash('success',`You have been logged out.`);
res.redirect('/');
});
// Signup
app.post('/signup', function(req,res,next){
User.findOne({'email':req.body.email}, function(err,user){
if (err){ next(err); }
// User already exists
else if (user){
req.flash('warning','A user with that email already exists! If you forgot your password, use <a href="/login/forgot">this form</a>.');
res.redirect('/login');
} else {
// Create user
var newUser = new User();
newUser.email = req.body.email;
newUser.created = Date.now();
newUser.createToken(function(err,token){
if (err){ next(err); }
mail({
from: '"Tracman" <NoReply@tracman.org>',
to: req.body.email,
subject: 'Complete your Tracman registration',
text: `Welcome to Tracman! \n\nTo complete your registration, follow this link and set your password:\n${env.url}/settings/password/${token}`,
html: `<p>Welcome to Tracman! </p><p>To complete your registration, follow this link and set your password:<br><a href="${env.url}/settings/password/${token}">${env.url}/settings/password/${token}</a></p>`
}).then(function(){
req.flash('success',`An email has been sent to <u>${req.body.email}</u>. Check your inbox to complete your registration.`);
res.redirect('/');
}).catch(function(err){
next(err);
});
app.get('/signup', (req,res)=>{
res.redirect('/login#signup');
}).post('/signup', (req,res,next)=>{
// Send token and alert user
function sendToken(user){
// Create a password token
user.createToken(function(err,token){
if (err){ mw.throwErr(err,req); }
// Email the instructions to continue
mail.send({
from: mail.from,
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}`),
html: mail.html(`<p>Welcome to Tracman! </p><p>To complete your registration, follow this link and set your password:<br><a href="${env.url}/settings/password/${token}">${env.url}/settings/password/${token}</a></p>`)
}).catch(function(err){
mw.throwErr(err,req);
res.redirect('/login#signup');
}).then(function(){
req.flash('success', `An email has been sent to <u>${user.email}</u>. Check your inbox to complete your registration. `);
res.redirect('/');
});
}
});
});
// Forgot password
app.route('/login/forgot')
.all( function(req,res,next){
if (req.isAuthenticated()){ res.redirect('/settings'); }
else { next(); }
})
.get( function(req,res,next){
res.render('forgot');
})
.post( function(req,res,next){
//TODO: Validate and sanitize email
// req.assert('email', 'Please enter a valid email address.').isEmail();
// req.sanitize('email').normalizeEmail({ remove_dots: false });
User.findOne({'email':req.body.email},function(err,user){
if (err){ next(err); }
else if (!user) {
req.flash('danger', `No user has <u>${req.body.email}</u> set as their email address. `);
res.redirect('/login/forgot');
} else {
// Set reset token to user
user.createToken( function(err,token){
if (err){ next(err); }
// Email reset link
mail({
from: '"Tracman" <NoReply@tracman.org>',
to: `"${user.name}"" <${user.email}>`,
subject: 'Reset your Tracman password',
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: `<p>Hi, </p><p>Did you request to reset your Tracman password? If so, follow this link to do so:<br><a href="${env.url}/settings/password/${token}">${env.url}/settings/password/${token}</a></p><p>If you didn't initiate this request, just ignore this email. </p>`
}).then(function(){
req.flash('success', `An email has been sent to <u>${req.body.email}</u>. Check your email for instructions to reset your password. `);
res.redirect('/');
}).catch(function(err){
next(err);
});
});
}
});
}
// Check if somebody already has that email
User.findOne({'email':req.body.email}, (err,user)=>{
if (err){ mw.throwErr(err,req); }
// User already exists
if (user && user.auth.password) {
req.flash('warning','A user with that email already exists! If you forgot your password, you can <a href="/login/forgot">reset it here</a>.');
res.redirect('/login#login');
next();
}
// User exists but hasn't created a password yet
else if (user) {
// Send another token (or the same one if it hasn't expired)
sendToken(user);
}
// Create user
else {
user = new User();
user.created = Date.now();
user.email = req.body.email;
user.slug = slug(user.email.substring(0, user.email.indexOf('@')));
// Generate unique slug
var generateSlug = new Promise((resolve,reject) => {
(function checkSlug(s,cb){
User.findOne({slug:s}, function(err, existingUser){
if (err) { mw.throwErr(err,req); }
// Slug in use: generate a random one and retry
if (existingUser){
s = '';
while (s.length<6) {
s+='abcdefghijkmnpqrtuvwxy346789'.charAt(Math.floor(Math.random()*28));
}
checkSlug(s,cb);
// Unique slug: proceed
} else { cb(s); }
});
})(user.slug, function(newSlug){
user.slug = newSlug;
resolve();
});
});
// Generate sk32
var generateSk32 = new Promise((resolve,reject) => {
crypto.randomBytes(32, (err,buf)=>{
if (err) { mw.throwErr(err,req); }
user.sk32 = buf.toString('hex');
resolve();
});
});
// Save user and send the token by email
Promise.all([generateSlug, generateSk32])
.catch(err => {
mw.throwErr(err,req);
}).then(() => {
user.save( (err)=>{
if (err){ mw.throwErr(err,req); }
sendToken(user);
});
});
}
});
});
// Forgot password
// app.route('/login/forgot')
// .all( (req,res,next)=>{
// if (req.isAuthenticated()){ res.redirect('/settings'); }
// else { next(); }
// })
// .get( (req,res,next)=>{
// res.render('forgot');
// })
// .post( (req,res,next)=>{
// //TODO: Validate and sanitize email
// // req.assert('email', 'Please enter a valid email address.').isEmail();
// // req.sanitize('email').normalizeEmail({ remove_dots: false });
// User.findOne( {'email':req.body.email}, (err,user)=>{
// if (err){ next(err); }
// else if (!user) {
// req.flash('danger', `No user has <u>${req.body.email}</u> set as their email address. `);
// res.redirect('/login/forgot');
// } else {
// // Set reset token to user
// user.createToken( (err,token)=>{
// if (err){ next(err); }
// // Email reset link
// mail({
// from: '"Tracman" <NoReply@tracman.org>',
// to: `"${user.name}"" <${user.email}>`,
// subject: 'Reset your Tracman password',
// 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: `<p>Hi, </p><p>Did you request to reset your Tracman password? If so, follow this link to do so:<br><a href="${env.url}/settings/password/${token}">${env.url}/settings/password/${token}</a></p><p>If you didn't initiate this request, just ignore this email. </p>`
// }).then(()=>{
// req.flash('success', `An email has been sent to <u>${req.body.email}</u>. Check your email for instructions to reset your password. `);
// res.redirect('/');
// }).catch((err)=>{
// next(err);
// });
// });
// }
// });
// });
// Social
app.get('/login/:service', function(req,res,next){
app.get('/login/:service', (req,res,next)=>{
var service = req.params.service;
if (service==='google'){
var sendParams = {scope:['profile']};
}
if (!req.user) { // Social login
// Social login
if (!req.user) {
passport.authenticate(service, sendParams)(req,res,next);
} else if (!req.user.auth[service]) { // Connect social account
}
// Connect social account
else if (!req.user.auth[service]) {
passport.authorize(service, sendParams)(req,res,next);
} else { // Disconnect social account
}
// Disconnect social account
else {
req.user.auth[service] = undefined;
req.user.save(function(err){
if (err){ mw.throwErr(err,req); }
req.user.save( (err)=>{
if (err) {
mw.throwErr(err,req);
res.redirect('/settings');
}
else {
req.flash('success', `${mw.capitalize(service)} account disconnected. `);
res.redirect('/settings');
}
res.redirect('/settings');
});
}
});
app.get('/login/:service/cb', function(req,res,next){
app.get('/login/:service/cb', (req,res,next)=>{
var service = req.params.service;
if (!req.user) {
passport.authenticate(service, loginOutcome)(req,res,next);
@ -151,123 +232,12 @@ module.exports = function(app, passport) {
passport.authenticate(service, connectOutcome)(req,res,next);
}
}, loginCallback);
// Old google auth
// app.get('/auth/google', passport.authenticate('google', { scope: [
// 'https://www.googleapis.com/auth/plus.login',
// 'https://www.googleapis.com/auth/plus.profile.emails.read'
// ] }));
// app.get('/auth/google/callback', passport.authenticate('google', {
// failureRedirect: '/',
// failureFlash: true,
// successRedirect: '/',
// successFlash: true
// } ));
// Android auth
//TODO: See if there's a better method
app.get('/auth/google/idtoken', passport.authenticate('google-id-token'), function (req,res) {
app.get('/auth/google/idtoken', passport.authenticate('google-id-token'), (req,res)=>{
if (!req.user) { res.sendStatus(401); }
else { res.send(req.user); }
} );
};
// passport.use(new GoogleStrategy({
// clientID: env.googleClientId,
// clientSecret: env.googleClientSecret,
// callbackURL: env.url+'/auth/google/callback',
// passReqToCallback: true
// }, function(req, accessToken, refreshToken, profile, done) {
// // Check for user
// User.findOne({googleID: profile.id}, function(err, user){
// // Error
// if (err) { console.log('Error finding user with google ID: '+profile.id+'\n'+err); }
// // User found
// if (!err && user !== null) /* Log user in */ {
// if (!user.name) { user.name=profile.displayName; }
// user.lastLogin = Date.now();
// user.save(function (err, raw) {
// if (err) { throwErr(err,req); }
// }); done(null, user);
// }
// // User not found
// else /* create user */ {
// user = new User();
// user.googleID = profile.id;
// user.name = profile.displayName;
// user.email = profile.emails[0].value;
// user.slug = slug(profile.displayName).toLowerCase();
// user.created = Date.now();
// user.lastLogin = Date.now();
// // user.settings = { units:'standard', defaultMap:'road', defaultZoom:11, showSpeed:false, showTemp:false, showAlt:false, showStreetview:false },
// // user.last = { lat:0, lon:0, dir:0, alt:0, spd:0 },
// // user.isPro = false;
// // user.isAdmin = false;
// var cbc = 2;
// var successMessage, failMessage;
// // Generate slug
// (function checkSlug(s,cb) {
// //console.log('checking ',s);
// User.findOne({slug:s}, function(err, existingUser){
// if (err) { console.log('No user found for ',slug,':',err); }
// if (existingUser){
// s = '';
// while (s.length<6) {
// s+='abcdefghijkmnpqrtuvwxy346789'.charAt(Math.floor(Math.random()*28));
// }
// checkSlug(s,cb);
// } else { cb(s); }
// });
// })(user.slug, function(newSlug){
// user.slug = newSlug;
// if (cbc>1) /* waiting on other calls */ { cbc--; }
// else { done(null, user, { success:successMessage, failure:failMessage }); }
// });
// // Generate sk32
// crypto.randomBytes(32, function(err,buf) {
// if (err) {console.log('Unable to get random bytes:',err);}
// if (!buf) {console.log('Unable to get random buffer');}
// else {
// user.sk32 = buf.toString('hex');
// user.save(function(err) {
// if (err) {
// console.log('Error saving new user '+err);
// var failMessage = 'Something went wrong creating your account. Would you like to <a href="/bug">report this error</a>?';
// } else { successMessage = 'Your account has been created. Next maybe you should download the <a href="/android">android app</a>. ' }
// if (cbc>1) /* waiting on other calls */ { cbc--; }
// else { done(null, user, { success:successMessage, failure:failMessage }); }
// });
// }
// });
// }
// });
// }));
// passport.use(new GoogleTokenStrategy({
// clientID: env.googleClientId
// }, function(parsedToken, googleId, done) {
// User.findOne({googleID:googleId}, function(err, user) {
// if (err) {
// console.log('Error finding user for gToken login with google profile ID: '+googleId+'\n'+err); }
// if (!err && user !== null) { // Log in
// user.lastLogin = Date.now();
// user.save(function (err) {
// if (err) {
// console.log('Error saving user\'s lastLogin for gToken login with google profile ID: '+googleId+'\n'+err); }
// });
// return done(err, user);
// } else { // No such user
// done(null, false);
// }
// });
// }));

View File

@ -17,39 +17,26 @@ let transporter = nodemailer.createTransport({
});
/* Confirm login */
// transporter.verify(function(err, success) {
// transporter.verify( (err,success)=>{
// if (err){ console.error(`SMTP Error: ${err}`); }
// console.log(`SMTP ${!success?'not ':''}ready...`);
// });
/* Send test email */
// transporter.sendMail({
// to: `"Keith Irwin" <hypergeek14@gmail.com>`,
// from: '"Tracman" <NoReply@tracman.org>',
// subject: 'Test email',
// text: "Looks like everything's working",
// html: ""
// }).then(function(){
// console.log("Email should have sent...");
// }).catch(function(err){
// console.error(err);
// });
// } );
module.exports = {
send: transporter.sendMail.bind(transporter),
text: function(text) {
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: function(text) {
html: (text)=>{
return `<h1><a href="/" style="text-decoration:none;"><span style="color:#000;font-family:sans-serif;font-size:36px;font-weight:bold"><img src="${env.url}/static/img/icon/by/32.png" alt="+" style="margin-right:10px">Tracman</span></a></h1>${text}<p style="font-size:8px;">Do not reply to this email. For information about why you recieved this email, see our <a href="${env.url}/privacy#email">privacy policy</a>. </p>`;
},
from: `"Tracman" <NoReply@tracman.org>`,
to: function(user) {
to: (user)=>{
return `"${user.name}" <${user.email}>`;
}

View File

@ -5,28 +5,30 @@ const env = require('./env.js');
module.exports = {
// Throw error
throwErr: function(err,req=null){
console.error('Middleware error:'+err.message+'\nfor request:\n'+req);
if (env.mode==='production') {
req.flash('danger', 'An error occured. <br>Would you like to <a href="https://github.com/Tracman-org/Server/issues/new">report it</a>?');
} else { // development
req.flash('danger', err);
throwErr: (err,req=null)=>{
console.error(`⛔️ ${err.stack}`);
if (req){
if (env.mode==='production') {
req.flash('danger', 'An error occured. <br>Would you like to <a href="https://github.com/Tracman-org/Server/issues/new">report it</a>?');
} else { // development
req.flash('danger', err.message);
}
}
},
// Capitalize the first letter of a string
capitalize: function(str){ 'use strict';
capitalize: (str)=>{
return str.charAt(0).toUpperCase() + str.slice(1);
},
// Ensure authentication
ensureAuth: function(req,res,next){
ensureAuth: (req,res,next)=>{
if (req.isAuthenticated()) { return next(); }
else { res.redirect('/login'); }
},
// Ensure administrator
ensureAdmin: function(req,res,next){
ensureAdmin: (req,res,next)=>{
if (req.user.isAdmin){ return next(); }
else { res.sendStatus(401); }
//TODO: test this by logging in as !isAdmin and go to /admin

View File

@ -6,7 +6,7 @@ const mongoose = require('mongoose'),
crypto = require('crypto');
const userSchema = new mongoose.Schema({
name: {type:String, required:true},
name: {type:String},
email: {type:String, required:true},
slug: {type:String, required:true, unique:true},
auth: {
@ -19,52 +19,52 @@ const userSchema = new mongoose.Schema({
},
isAdmin: {type:Boolean, required:true, default:false},
isPro: {type:Boolean, required:true, default:false},
created: Date,
created: {type:Date, required:true},
lastLogin: Date,
settings: {
units: {type:String, default:'standard'},
defaultMap: {type:String, default:'road'},
defaultZoom: {type:Number, default:11},
showSpeed: {type:Boolean, default:false},
showTemp: {type:Boolean, default:false},
showAlt: {type:Boolean, default:false},
showStreetview: {type:Boolean, default:false}
units: {type:String, required:true, default:'standard'},
defaultMap: {type:String, required:true, default:'road'},
defaultZoom: {type:Number, required:true, default:11},
showSpeed: {type:Boolean, required:true, default:false},
showTemp: {type:Boolean, required:true, default:false},
showAlt: {type:Boolean, required:true, default:false},
showStreetview: {type:Boolean, required:true, default:false}
},
last: {
time: Date,
lat: {type:Number, default:0},
lon: {type:Number, default:0},
dir: {type:Number, default:0},
alt: {type:Number, default:0},
spd: {type:Number, default:0}
lat: {type:Number, required:true, default:0},
lon: {type:Number, required:true, default:0},
dir: {type:Number, required:true, default:0},
alt: {type:Number, required:true, default:0},
spd: {type:Number, required:true, default:0}
},
sk32: {type:String, required:true, unique:true}
}).plugin(unique);
/* User methods */ {
// Generate hash for new password
userSchema.methods.generateHash = function(password, next) {
bcrypt.genSalt(8, function(err,salt){
userSchema.methods.generateHash = (password,next)=>{
bcrypt.genSalt(8, (err,salt)=>{
if (err){ return next(err); }
bcrypt.hash(password, salt, null, next);
});
};
// Create password reset token
userSchema.methods.createToken = function(next){
userSchema.methods.createToken = (next)=>{
var user = this;
if ( user.auth.tokenExpires <= Date.now() ){
// Reuse old token, resetting clock
user.auth.tokenExpires = Date.now() + 3600000; // 1 hour
user.save();
return next(null.user.auth.passToken);
} else {
// Create new token
crypto.randomBytes(16, function(err,buf){
crypto.randomBytes(16, (err,buf)=>{
if (err){ next(err,null); }
else {
user.auth.passToken = buf.toString('hex');
@ -73,15 +73,15 @@ const userSchema = new mongoose.Schema({
return next(null,user.auth.passToken);
}
});
}
};
// Check for valid password
userSchema.methods.validPassword = function(password, next) {
userSchema.methods.validPassword = (password,next)=>{
bcrypt.compare(password, this.auth.password, next);
};
}
module.exports = {

View File

@ -9,81 +9,55 @@ const
mw = require('./middleware.js'),
User = require('./models.js').user;
module.exports = function(passport) {
module.exports = (passport)=>{
// Serialize/deserialize users
passport.serializeUser(function(user,done) {
passport.serializeUser((user,done)=>{
done(null, user.id);
});
passport.deserializeUser(function(id,done) {
User.findById(id, function(err, user) {
passport.deserializeUser((id,done)=>{
User.findById(id, (err,user)=>{
if(!err){ done(null, user); }
else { done(err, null); }
});
});
// Signup
// passport.use('signup', new LocalStrategy({
// usernameField: 'email',
// passwordField: 'password',
// passReqToCallback : true
// }, function(req, email, password, done) {
// process.nextTick(function() {
// User.findOne({'email':email }, function(err, user) {
// if (err){ return done(err); }
// // Check for existing user
// if (user) {
// return done( null, false, req.flash('warning','That email is already in use. Try logging in below.') );
// // Create user
// } else {
// var newUser = new User();
// newUser.email = email;
// newUser.created = Date.now();
// newUser.lastLogin = Date.now();
// newUser.generateHash(password, function(err, hash){
// if (err){ return done(err); }
// newUser.auth.password = hash;
// newUser.save(function(err) {
// if (err){ return done(err); }
// return done( null, newUser );
// });
// });
// }
// });
// });
// })
// );
// Local
passport.use('local', new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallback : true
}, function(req, email, password, done) {
User.findOne({ 'email':email }, function (err, user) {
passReqToCallback: true
}, (req,email,password,done)=>{
User.findOne( {'email':email}, (err,user)=>{
if (err){ return done(err); }
// Wrong username
// No user with that email
if (!user) {
return done( null, false, req.flash('danger','No account exists for that email.') );
// Username correct, password incorrect
} else {
return done( null, false, req.flash('danger','Incorrect email or password.') );
}
// User exists
else {
// Check password
user.validPassword(password, function(err,res){
if (err){ console.log('Passport error:\n',err); }
if (!res) { // Password incorrect
return done( null, false, req.flash('danger','Incorrect password.') );
} else { // Successful login
user.validPassword( password, (err,res)=>{
if (err){ return done(err); }
// Password incorrect
if (!res) {
return done( null, false, req.flash('danger','Incorrect email or password.') );
}
// Successful login
else {
user.lastLogin = Date.now();
user.save();
return done( null, user );
return done(null,user);
}
});
} );
}
});
} );
}
));
@ -96,35 +70,39 @@ module.exports = function(passport) {
var query = {};
query['auth.'+service] = profileId;
User.findOne(query, function (err, user) {
User.findOne(query, (err,user)=>{
if (err){ return done(err); }
// Can't find user
else if (!user){
// console.log('User not found.');
// Lazy update from old googleId field
if (service==='google') {
User.findOne({'googleID':parseInt(profileId)}, function(err,user){
// console.log(`searched for user with googleID ${profileId}`);
if (err){ mw.throwErr(err,req); }
User.findOne( {'googleID':parseInt(profileId)}, (err,user)=>{
if (err){ return done(err); }
if (user) {
// console.log(`Lazily updating schema for ${user.name}.`);
user.auth.google = profileId;
user.googleId = null;
user.save(function(err){
user.save( (err)=>{
if (err){ mw.throwErr(err,req); }
else { console.info(`🗂️ Lazily updated schema for ${user.name}.`); }
return done(null, user);
});
} );
} else {
req.flash('danger',`There's no user for that ${service} account. `);
return done();
}
});
} else {
} );
}
// No googleId either
else {
req.flash('danger',`There's no user for that ${service} account. `);
return done();
}
}
// Successfull social login
else {
// console.log(`Found user: ${user}`);
return done(null, user);
@ -134,12 +112,12 @@ module.exports = function(passport) {
// Connect account
else {
console.log(`Connecting ${service} account.`);
// console.log(`Connecting ${service} account.`);
req.user.auth[service] = profileId;
req.user.save(function(err){
req.user.save( (err)=>{
if (err){ return done(err); }
else { return done(null, req.user); }
});
} );
}
}
@ -150,7 +128,7 @@ module.exports = function(passport) {
clientSecret: env.googleClientSecret,
callbackURL: env.url+'/login/google/cb',
passReqToCallback: true
}, function(req, accessToken, refreshToken, profile, done) {
}, (req, accessToken, refreshToken, profile, done)=>{
socialLogin(req, 'google', profile.id, done);
}
));
@ -161,7 +139,7 @@ module.exports = function(passport) {
clientSecret: env.facebookAppSecret,
callbackURL: env.url+'/login/facebook/cb',
passReqToCallback: true
}, function(req, accessToken, refreshToken, profile, done) {
}, (req, accessToken, refreshToken, profile, done)=>{
socialLogin(req, 'facebook', profile.id, done);
}
));
@ -172,7 +150,7 @@ module.exports = function(passport) {
consumerSecret: env.twitterConsumerSecret,
callbackURL: env.url+'/login/twitter/cb',
passReqToCallback: true
}, function(req, token, tokenSecret, profile, done) {
}, (req, token, tokenSecret, profile, done)=>{
socialLogin(req, 'twitter', profile.id, done);
}
));

View File

@ -2,69 +2,35 @@
const router = require('express').Router(),
mw = require('../middleware.js'),
User = require('../models.js').user,
mail = require('../mail.js');
User = require('../models.js').user;
router.route('/')
.all(mw.ensureAdmin, function(req,res,next){
.all(mw.ensureAdmin, (req,res,next)=>{
next();
}).get(function(req,res){
var cbc = 0;
var checkCBC = function(req,res,err){
if (err) {
req.flash('error', err.message);
console.error(err);
}
if (cbc<1){ cbc++; }
else { // done
res.render('admin', {
noFooter: '1',
success:req.flash('success')[0],
error:req.flash('error')[0]
});
}
};
User.findById(req.user, function(err, found) {
res.locals.user = found;
checkCBC(req,res,err);
});
User.find({}).sort({lastLogin:-1}).exec(function(err, found){
res.locals.users = found;
checkCBC(req,res,err);
});
});
} )
router.route('/users')
.all(mw.ensureAdmin, function(req,res,next){
next();
}).post(function(req,res,next){
.get( (req,res)=>{
User.find({}).sort({lastLogin:-1})
.catch( (err)=>{
mw.throwErr(err);
}).then( (found)=>{
res.render('admin', {
noFooter: '1',
users: found
});
});
} )
.post( (req,res,next)=>{
if (req.body.delete) {
User.findOneAndRemove({'_id':req.body.delete}, function(err,user){
User.findOneAndRemove( {'_id':req.body.delete}, (err,user)=>{
if (err){ req.flash('error', err.message); }
else { req.flash('success', '<i>'+user.name+'</i> deleted.'); }
res.redirect('/admin#users');
});
} else { console.error('ERROR! POST without action sent. '); next(); }
});
router.route('/testmail').get(function(req,res,next){
mail.send({
to: `"Keith Irwin" <hypergeek14@gmail.com>`,
from: mail.from,
subject: 'Test email',
text: mail.text("Looks like everything's working! "),
html: mail.html("<p>Looks like everything's working! </p>")
}).then(function(){
console.log("Test email should have sent...");
res.sendStatus(200);
}).catch(function(err){
mw.throwErr(err,req);
next();
});
});
} );
} else { console.error(new Error('POST without action sent. ')); next(); }
} );
module.exports = router;

View File

@ -6,24 +6,25 @@ const mw = require('../middleware.js'),
User = require('../models.js').user;
// Index
router.get('/', function(req,res,next) {
router.get('/', (req,res,next)=>{
res.render('index');
});
// Help
router.get('/help', mw.ensureAuth, function(req,res){
res.render('help');
});
router.get('/help', mw.ensureAuth, (req,res)=>{
res.render('help');
});
// Terms of Service and Privacy Policy
router.get('/terms', function(req,res){
router.get('/terms', (req,res)=>{
res.render('terms');
}).get('/privacy', function(req,res){
})
.get('/privacy', (req,res)=>{
res.render('privacy');
});
// robots.txt
router.get('/robots.txt', function(req,res){
router.get('/robots.txt', (req,res)=>{
res.type('text/plain');
res.send("User-agent: *\n"+
"Disallow: /map/*\n"
@ -31,28 +32,28 @@ router.get('/robots.txt', function(req,res){
});
// favicon.ico
router.get('/favicon.ico', function(req,res){
router.get('/favicon.ico', (req,res)=>{
res.redirect('/static/img/icon/by/16-32-48.ico');
});
// Endpoint to validate forms
router.get('/validate', function(req,res){
router.get('/validate', (req,res)=>{
if (req.query.slug) { // validate unique slug
User.findOne({slug:slug(req.query.slug)}, function(err, existingUser){
User.findOne( {slug:slug(req.query.slug)}, (err,existingUser)=>{
if (err) { console.log('/validate error:',err); }
if (existingUser && existingUser.id!==req.user) { res.sendStatus(400); }
else { res.sendStatus(200); }
});
} );
}
});
// Link to androidapp in play store
router.get('/android', function(req,res){
router.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
router.get('/ios', function(req,res){
router.get('/ios', (req,res)=>{
res.sendStatus(404);
//TODO: Add link to info about why there's no ios app
});

View File

@ -1,4 +1,5 @@
'use strict';
//TODO: Use promises
const router = require('express').Router(),
mw = require('../middleware.js'),
@ -6,12 +7,12 @@ const router = require('express').Router(),
User = require('../models.js').user;
// Redirect to real slug
router.get('/', mw.ensureAuth, function(req,res){
router.get('/', mw.ensureAuth, (req,res)=>{
res.redirect(`/map/${req.user.slug}`);
});
// Show map
router.get('/:slug?', function(req,res,next){
router.get('/:slug?', (req,res,next)=>{
var mapuser='', user='', cbc=0;
// Confirm sucessful queries

View File

@ -11,20 +11,20 @@ const slug = require('slug'),
// Settings form
router.route('/')
.all(mw.ensureAuth, function(req,res,next){
.all( mw.ensureAuth, (req,res,next)=>{
next();
})
} )
// Get settings form
.get(function(req,res,next){
User.findById(req.user, function(err,user){
.get( (req,res,next)=>{
User.findById( req.user, (err,user)=>{
if (err){ mw.throwErr(err,req); }
res.render('settings');
});
})
} );
} )
// Set new settings
.post(function(req,res,next){
.post( (req,res,next)=>{
User.findByIdAndUpdate(req.user, {$set:{
name: xss(req.body.name),
slug: slug(xss(req.body.slug)),
@ -37,65 +37,71 @@ router.route('/')
showAlt: (req.body.showAlt)?true:false,
showStreetview: (req.body.showStreet)?true:false
}
}}, function(err, user){
if (err) { mw.throwErr(err,req); }
else { req.flash('success', 'Settings updated. '); }
res.redirect('/settings');
});
})
}}, (err,user)=>{
if (err) {
mw.throwErr(err,req);
res.redirect('/settings');
}
else {
req.flash('success', 'Settings updated. ');
res.redirect('/settings');
}
});
} )
// Delete user account
.delete(function(req,res,next){
User.findByIdAndRemove( req.user,
function(err) {
if (err) {
mw.throwErr(err,req);
} else {
req.flash('success', 'Your account has been deleted. ');
}
.delete( (req,res,next)=>{
User.findByIdAndRemove( req.user, (err)=>{
if (err) {
mw.throwErr(err,req);
res.redirect('/settings');
} else {
req.flash('success', 'Your account has been deleted. ');
res.redirect('/');
}
);
});
} );
} );
// Set password
router.route('/password/')
.all(mw.ensureAuth,function(req,res,next){
.all( mw.ensureAuth, (req,res,next)=>{
next();
})
} )
// Email user a token, proceed at /password/:token
.get(function(req,res,next){
.get( (req,res,next)=>{
// Create token for password change
req.user.createToken(function(err,token){
req.user.createToken( (err,token)=>{
if (err){ next(err); }
// Confirm password change request by email.
// Confirm password change request by email.
mail.send({
to: mail.to(req.user),
from: mail.from,
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 in 1 hour. `),
html: mail.html(`<p>A request has been made to change your tracman password. If you did not initiate this request, please contact support at <a href="mailto:keith@tracman.org">keith@tracman.org</a>. </p><p>To change your password, follow this link:<br><a href="${env.url}/settings/password/${token}">${env.url}/settings/password/${token}</a>. </p><p>This request will expire in 1 hour. </p>`)
}).catch(function(err){
}).catch( err=>{
mw.throwErr(err,req);
}).then(function(){
// Alert user to check email.
res.redirect('/login#login');
}).then( ()=>{
// Alert user to check email.
req.flash('success',`An email has been sent to <u>${req.user.email}</u>. Check your inbox to complete your password change. `);
res.redirect( (req.isAuthenticated)?'/settings':'/login' );
res.redirect('/login#login');
});
});
});
} );
} );
router.route('/password/:token')
// Check token
.all(function(req,res,next){
.all( (req,res,next)=>{
User
.findOne({'auth.passToken': req.params.token})
.where('auth.tokenExpires').gt(Date.now())
@ -110,71 +116,66 @@ router.route('/password/:token')
next();
}
});
})
} )
// Show password change form
.get(function(req,res){
.get( (req,res)=>{
res.render('password');
})
.post(function(req,res,next){
} )
.post( (req,res,next)=>{
// Validate matching passwords
if (req.body.password!==req.body.repassword) {
mw.throwErr( new Error('Passwords do not match. '), req );
} else {
//TODO: Validate password
//TODO: Add logic for new users
// Delete token
res.locals.passwordUser.auth.passToken = undefined;
res.locals.passwordUser.auth.tokenExpires = undefined;
// Create hash
res.locals.passwordUser.generateHash(req.body.password, function(err,hash){
if (err){ mw.throwErr(err); }
else {
// Save new password to db
res.locals.passwordUser.auth.password = hash;
res.locals.passwordUser.save( function(err){
if (err){ mw.throwErr(err,req); }
else {
req.flash('success', 'Password set. You can use it to log in now. ');
}
});
}
});
}
// Delete token
res.locals.passwordUser.auth.passToken = undefined;
res.locals.passwordUser.auth.tokenExpires = undefined;
res.redirect('/login');
// Create hash
res.locals.passwordUser.generateHash( req.body.password, (err,hash)=>{
if (err){ mw.throwErr(err,req); }
else {
// Save new password to db
res.locals.passwordUser.auth.password = hash;
res.locals.passwordUser.save( (err)=>{
if (err){
mw.throwErr(err,req);
res.redirect('/login#signup');
}
else {
req.flash('success', 'Password set. You can use it to log in now. ');
res.redirect('/login#login');
}
});
}
} );
});
} );
// Tracman pro
router.route('/pro')
.all(mw.ensureAuth, function(req,res,next){
.all( mw.ensureAuth, (req,res,next)=>{
next();
})
} )
// Get info about pro
.get(function(req,res,next){
.get( (req,res,next)=>{
res.render('pro');
})
} )
// Join Tracman pro
.post(function(req,res){
.post( (req,res)=>{
User.findByIdAndUpdate(req.user.id,
{$set:{ isPro:true }},
function(err, user){
(err,user)=>{
if (err){ mw.throwErr(err,req); }
else { req.flash('success','You have been signed up for pro. '); }
res.redirect('/map');
}
);
});
} );
module.exports = router;

View File

@ -6,24 +6,24 @@ const router = require('express').Router(),
router
.get('/mail', function(req,res,next){
.get('/mail', (req,res,next)=>{
mail.send({
to: `"Keith Irwin" <hypergeek14@gmail.com>`,
from: mail.from,
subject: 'Test email',
text: mail.text("Looks like everything's working! "),
html: mail.html("<p>Looks like everything's working! </p>")
}).then(function(){
}).then(()=>{
console.log("Test email should have sent...");
res.sendStatus(200);
}).catch(function(err){
}).catch((err)=>{
mw.throwErr(err,req);
next();
});
})
.get('/password', function(req,res){
.get('/password', (req,res)=>{
res.render('password');
})
});
module.exports = router;

View File

@ -1,7 +1,8 @@
'use strict';
// Imports
const User = require('./models.js').user;
const mw = require('./middleware.js'),
User = require('./models.js').user;
// Check for tracking clients
function checkForUsers(io, user) {
@ -9,9 +10,9 @@ function checkForUsers(io, user) {
// Checks if any sockets are getting updates for this user
//TODO: Use Object.values() after upgrading to node v7
if (Object.keys(io.sockets.connected).map( function(id){
if (Object.keys(io.sockets.connected).map( (id)=>{
return io.sockets.connected[id];
}).some( function(socket){
}).some( (socket)=>{
return socket.gets==user;
})) {
//console.log(`Activating updates for ${user}.`);
@ -26,56 +27,56 @@ module.exports = {
checkForUsers: checkForUsers,
init: function(io){
io.on('connection', function(socket) {
init: (io)=>{
io.on('connection', (socket)=>{
//console.log(`${socket.id} connected.`);
// Log
//socket.on('log', function(text){
/* Log */
//socket.on('log', (text)=>{
//console.log(`LOG: ${text}`);
//});
// This socket can set location (app)
socket.on('can-set', function(userId){
socket.on('can-set', (userId)=>{
//console.log(`${socket.id} can set updates for ${userId}.`);
socket.join(userId, function(){
socket.join(userId, ()=>{
//console.log(`${socket.id} joined ${userId}`);
});
checkForUsers( io, userId );
});
// This socket can receive location (map)
socket.on('can-get', function(userId){
socket.on('can-get', (userId)=>{
socket.gets = userId;
//console.log(`${socket.id} can get updates for ${userId}.`);
socket.join(userId, function(){
socket.join(userId, ()=>{
//console.log(`${socket.id} joined ${userId}`);
socket.to(userId).emit('activate', 'true');
});
});
// Set location
socket.on('set', function(loc){
socket.on('set', (loc)=>{
//console.log(`${socket.id} set location for ${loc.usr}`);
loc.time = Date.now();
// Check for sk32 token
if (!loc.tok) { console.log('!loc.tok for loc:',loc) }
if (!loc.tok) { mw.throwErr(new Error(`⛔️ !loc.tok for loc: ${loc}`)) }
else {
// Get loc.usr
User.findById(loc.usr, function(err, user) {
if (err) { console.log('Error finding user:',err); }
if (!user) { console.log('User not found for loc:',loc); }
else {
User.findById(loc.usr, (err,user)=>{
if (err) { mw.throwErr(err); }
if (user) {
// Confirm sk32 token
if (loc.tok!=user.sk32) { console.log('loc.tok!=user.sk32 || ',loc.tok,'!=',user.sk32); }
if (loc.tok!=user.sk32) { mw.throwErr(new Error(`⛔️ loc.tok!=user.sk32\n\t${loc.tok} != ${user.sk32}`)); }
else {
// Broadcast location
io.to(loc.usr).emit('get', loc);
//console.log(`Broadcasting ${loc.lat}, ${loc.lon} to ${loc.usr}`);
// Save in db as last seen
user.last = {
lat: parseFloat(loc.lat),
@ -84,9 +85,9 @@ module.exports = {
spd: parseFloat(loc.spd||0),
time: loc.time
};
user.save(function(err) {
if (err) { console.log('Error saving user last location:'+loc.user+'\n'+err); }
});
user.save( (err)=>{
if (err) { mw.throwErr(err); }
} );
}
}
@ -96,7 +97,7 @@ module.exports = {
});
// Shutdown (check for remaining clients)
socket.on('disconnect', function(reason){
socket.on('disconnect', (reason)=>{
//console.log(`${socket.id} disconnected because of a ${reason}.`);
// Check if client was receiving updates
@ -108,8 +109,8 @@ module.exports = {
});
// Log errors
socket.on('error', function(err){
console.log('Socket error! ',err);
socket.on('error', (err)=>{
mw.throwErr(err);
});
});

103
server.js
View File

@ -1,7 +1,7 @@
'use strict';
/* IMPORTS */
const
/* IMPORTS */
const
express = require('express'),
bodyParser = require('body-parser'),
cookieParser = require('cookie-parser'),
@ -11,6 +11,7 @@ const
passport = require('passport'),
flash = require('connect-flash'),
env = require('./config/env.js'),
mw = require('./config/middleware.js'),
User = require('./config/models.js').user,
app = express(),
http = require('http').Server(app),
@ -19,14 +20,18 @@ const
/* SETUP */ {
/* Database */ mongoose.connect(env.mongoSetup, {
server:{socketOptions:{
keepAlive:1, connectTimeoutMS:30000 }},
replset:{socketOptions:{
keepAlive:1, connectTimeoutMS:30000 }}
}).catch((err)=>{
mw.throwErr(err);
}).then(()=>{
console.log(`💿 Mongoose connected to database`);
});
/* Templates */ {
nunjucks.configure(__dirname+'/views', {
autoescape: true,
@ -34,7 +39,7 @@ const
});
app.set('view engine','html');
}
/* Session */ {
app.use(cookieParser(env.cookie));
app.use(cookieSession({
@ -49,7 +54,7 @@ const
}));
app.use(flash());
}
/* Auth */ {
require('./config/passport.js')(passport);
app.use(passport.initialize());
@ -58,116 +63,116 @@ const
// app.use(passport.initialize());
// app.use(passport.session());
// require('./config/auth.js');
// passport.serializeUser(function(user,done) {
// passport.serializeUser( (user,done)=>{
// done(null, user.id);
// });
// passport.deserializeUser(function(id,done) {
// User.findById(id, function(err, user) {
// } );
// passport.deserializeUser( (id,done)=>{
// User.findById( id, (err,user)=>{
// if(!err) done(null, user);
// else done(err, null);
// });
// });
// } );
// } );
}
/* Routes */ {
// Static files (keep this before setting default locals)
app.use('/static', express.static(__dirname+'/static'));
app.use('/static', express.static( __dirname+'/static', {dotfiles:'allow'} ));
// Set default locals available to all views (keep this after static files)
app.get('/*', function(req,res,next){
app.get( '/*', (req,res,next)=>{
// console.log(`Setting local variables for request to ${req.path}.`);
// User account
res.locals.user = req.user;
// console.log(`User set as ${res.locals.user}. `);
// Flash messages
res.locals.successes = req.flash('success');
res.locals.dangers = req.flash('danger');
res.locals.warnings = req.flash('warning');
// console.log(`Flash messages set as:\nSuccesses: ${res.locals.successes}\nWarnings: ${res.locals.warnings}\nDangers: ${res.locals.dangers}`);
next();
});
} );
// Main routes
app.use( '/', require('./config/routes/index.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(function(req,res,next) {
app.use( (req,res,next)=>{
if (!res.headersSent) {
var err = new Error('404: Not found: '+req.url);
var err = new Error('404 Not found: '+req.url);
err.status = 404;
next(err);
}
});
} );
// Handlers
if (env.mode=='production') {
app.use(function(err,req,res,next) {
app.use( (err,req,res,next)=>{
if (res.headersSent) { return next(err); }
res.status(err.status||500);
res.render('error', {
code: err.status
});
});
} );
}
else /* Development */{
app.use(function(err,req,res,next) {
app.use( (err,req,res,next)=>{
console.log(err);
if (res.headersSent) { return next(err); }
res.status(err.status||500);
res.render('error', {
code: err.status,
message: err.message,
error: err
error: err.stack
});
});
} );
}
}
/* Sockets */ {
sockets.init(io);
}
}
/* RUNTIME */ {
console.log('Starting Tracman server...');
console.log('🖥 Starting Tracman server...');
// Listen
http.listen(env.port, function(){
console.log(`Listening in ${env.mode} mode on port ${env.port}. `);
http.listen( env.port, ()=>{
console.log(`🌐 Listening in ${env.mode} mode on port ${env.port}... `);
// Check for clients for each user
User.find({}, function(err, users){
User.find( {}, (err,users)=>{
if (err) { console.log(`DB error finding all users: ${err.message}`); }
users.forEach( function(user){
users.forEach( (user)=>{
sockets.checkForUsers( io, user.id );
});
});
});
}
module.exports = app;

View File

@ -51,7 +51,10 @@ header nav {
} header nav ul li a:hover,
header nav ul li a:focus,
header nav ul li a.active,
header .logo:hover {
text-decoration: none;
background: rgba(255,255,255,0.1);
}
/* Hamburger */
header .hamburger {
@ -72,8 +75,8 @@ header .hamburger {
} header .hamburger-inner {
top: 50%;
margin-top: -2px;
} header .hamburger-inner,
header .hamburger-inner::before,
} header .hamburger-inner,
header .hamburger-inner::before,
header .hamburger-inner::after {
width: 40px;
height: 4px;

81
test.js
View File

@ -5,15 +5,7 @@ const chai = require('chai'),
chai.use(chaiHttp);
describe('Index', function() {
// I think this restarts the server after each try?
// var server;
// beforeEach(function() {
// server = require('./server');
// });
// afterEach(function() {
// server.close();
// });
describe('Pages', function() {
it('Displays homepage', function(done){
request(server).get('/')
@ -21,6 +13,24 @@ describe('Index', function() {
.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)
@ -36,54 +46,13 @@ describe('Index', function() {
});
// describe('Auth', function() {
describe('Auth', function() {
// it('Creates an account', function(done){
// request(server).get('/login')
// .expect(200)
// .end(function(err,res){
// //TODO: google authentication
// it('Logs out', function(done){
// request(server).get('/logout')
// .expect(200)
// .end(function(err,res){
// it('Logs in', function(done){
// request(server).get('/logout')
// .expect(200)
// .end(function(err,res){
// cbc=2;
// var deletesAccount = function(done){
// it('Deletes own account', function(){
// //TODO: Delete account via GUI
// });
// }
// it('Shows own map', function(done){
// request(server).get('/map')
// .expect(200)
// //TODO: Expect no js errors
// .end(function(err,res){
// if (cbc<2){ deletesAccount(); }
// else { cbc--; }
// done();
// });
// });
// it('Has the correct account info', function(done){
// //TODO: Check account info
// if (cbc<2){ deletesAccount(); }
// else { cbc--; }
// done();
// });
// done();
// });
// });
// done();
// });
// });
// done();
// });
// });
it('Creates an account', function(done){
request(server).post('/signup',{"email":"test@tracman.org"})
.expect(200)
.end(function(err,res){ done(); });
});
//TODO: it('Has the correct account info', function(done){

View File

@ -37,7 +37,7 @@
<a href="https://plus.google.com/{{usr.googleID}}/">Google</a>
{% endif %}
</td>
<td id='{{usr.id}}-edit'><form action="/admin/users" method="POST">
<td id='{{usr.id}}-edit'><form method="POST">
<button type="submit" class='btn btn-block btn-danger' name="delete" value="{{usr.id}}">DELETE</button>
</form></td>
</tr>

View File

@ -1,14 +1,12 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | {{ code }} Error{% endblock %}
{% block title %}{{super()}} | Error{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
{% if code %}<h2>{{code}}</h2>{% endif %}
{% if message %}<h3>{{message}}</h3>{% endif %}
{% if error %}<p>{{error}}</p>{% endif %}
<p>I would really appreciate it if you would <a href="https://github.com/Tracman-org/Server/issues/new">report this error</a>. </p>
{% if code %}<img style="width:100%" src="https://http.cat/{{code}}.jpg">{% endif %}
</div>
<section class='container'>
{% if code %}<h2>{{code}}</h2>{% endif %}
{% if message %}<h3>{{message}}</h3>{% endif %}
{% if stack %}<p>{{stack}}</p>{% else %}
<p>I would really appreciate it if you would <a href="https://github.com/Tracman-org/Server/issues/new">report this error</a>. </p>{% endif %}
{% if code %}<img style="width:100%" src="https://http.cat/{{code}}.jpg">{% endif %}
</section>
{% endblock %}

View File

@ -1,15 +1,15 @@
<header class='shadow'>
<!-- Logo -->
<a href="/"><span class='logo'><img class='icon' src="/static/img/style/logo-28.png" alt="+">Tracman</span></a>
<!-- Hamburger -->
<div class='hamburger hamburger--slider' aria-label="Menu" aria-controls="navigation">
<div class='hamburger-box'>
<div class='hamburger-inner'></div>
</div>
</div>
<!-- Navigation -->
<nav id='navigation'><ul>
{% if user %}
@ -25,7 +25,7 @@
<li><a href="/login#signup">Join</a></li>
{% endif %}
</ul></nav>
</header>
<!-- Flash messages -->