From 7153f7bb9b54d72f13dbcbad115b923fbee0d9f3 Mon Sep 17 00:00:00 2001 From: Keith Irwin Date: Sat, 1 Apr 2017 13:03:05 -0400 Subject: [PATCH] Added login screen --- PRIVACY.md | 13 -- README.md | 12 +- config/auth.js | 350 ++++++++++++++++++++++++++---------- config/middleware.js | 65 ++++--- config/models/user.js | 35 ---- config/routes/admin.js | 4 +- config/routes/auth.js | 34 ---- config/routes/index.js | 18 +- config/routes/map.js | 13 +- config/routes/misc.js | 2 +- config/sockets.js | 2 +- package.json | 6 +- server.js | 65 ++++--- static/css/base.css | 323 +++++++++++++++++---------------- static/css/footer.css | 10 +- static/css/header.css | 131 +++++++------- static/js/header.js | 6 + views/index.html | 2 +- views/login.html | 79 +++++--- views/templates/base.html | 39 ++-- views/templates/header.html | 31 ++-- 21 files changed, 688 insertions(+), 552 deletions(-) delete mode 100644 PRIVACY.md delete mode 100644 config/models/user.js delete mode 100644 config/routes/auth.js diff --git a/PRIVACY.md b/PRIVACY.md deleted file mode 100644 index ff05347..0000000 --- a/PRIVACY.md +++ /dev/null @@ -1,13 +0,0 @@ -# Privacy Policy for Tracman 0.3.1 - -In lieu of legalease, which I don't speak, here is a quick rundown of what Tracman does with your data (such as location). - -## Location history - -Your location is saved on the database as long as you have it "set" or "tracking". If you "clear" the data, it will be deleted from the database too. This doesn't mean all copies are destroyed. Our servers keep occasional backups, and caches could exist on other servers (google index, wayback archive, etc). - -This means that all public access to your location is essentially deleted when you clear it. But anyone could record your location while it's publicly available and rebroadcast it. Tracman doesn't store location histories (except as mentioned above), but histories may exist elsewhere! If you have (or plan to have) trouble with the law, don't use Tracman. Authorities have easy access to those histories. - -## Email addresses - -Tracman stores email addresses so we can contact users for important stuff (urgent security updates, deletion requests, lost passwords). We will never subscribe you to anything else by default. diff --git a/README.md b/README.md index 745967b..d68583d 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,14 @@ node.js application to display a map with user's location. $ git clone https://github.com/Tracman-org/Server.git && (cd Server && exec npm install) ``` -You will need to set up a configuration file at `config/secrets.js`. It should contain the following information: +You will need to set up a configuration file at `config/env.js`. It should contain the following information: ```javascript 'use strict'; module.exports = { - env: 'development', // or 'production' + mode: 'development', // or 'production' // Random strings to prevent hijacking session: 'this is a secret', @@ -25,8 +25,12 @@ module.exports = { // Client IDs for authentication googleClientId: '############-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com', googleClientSecret: 'XXXXXXXXX_XXXXXXXXXXXXXX', + facebookAppId: 'XXXXXXXXXXXXXXXX', + facebookAppSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + twitterConsumerKey: 'XXXXXXXXXXXXXXXXXXXXXXXXX', + twitterConsumerSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - // A google maps API + // A google maps API key googleMapsAPI: 'XXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXX', // Location of your mongoDB @@ -39,6 +43,8 @@ module.exports = { }; ``` +Use `config/env-sample.js` for help. + You can get API keys at the [google developer's console](https://console.developers.google.com/apis/credentials). You will need to set up approved hosts and auth callbacks. There is more information in [their documentation](https://support.google.com/googleapi/answer/6158857?hl=en). ## Running diff --git a/config/auth.js b/config/auth.js index c2f4cbf..8501356 100644 --- a/config/auth.js +++ b/config/auth.js @@ -1,108 +1,270 @@ 'use strict'; -const passport = require('passport'), - slug = require('slug'), - crypto = require('crypto'), - secret = require('./secrets.js'), - User = require('./models/user.js'), - GoogleStrategy = require('passport-google-oauth2').Strategy, - GoogleTokenStrategy = require('passport-google-id-token'); +const mw = require('./middleware.js'), + mail = require('./mail.js'), + User = require('./models.js').user, + env = require('./env.js'); -passport.use(new GoogleStrategy({ - clientID: secret.googleClientId, - clientSecret: secret.googleClientSecret, - callbackURL: secret.url+'/auth/google/callback', - passReqToCallback: true -}, function(req, accessToken, refreshToken, profile, done) { +module.exports = function(app, passport) { + + // Methods for success and failure + var loginOutcome = { + failureRedirect: '/login', + failureFlash: true + }; + var connectOutcome = { + failureRedirect: '/account', + failureFlash: true + }; + var loginCallback = function(req,res){ + res.redirect( req.session.returnTo || '/settings' ); + delete req.session.returnTo; + }; + + // Login/-out + app.route('/login') + .get( function(req,res){ + if (req.isAuthenticated()){ + res.redirect('/account'); } + else { res.render('login'); } + }) + .post( passport.authenticate('local',loginOutcome), loginCallback ); + app.get('/logout', function(req,res){ + req.logout(); + res.redirect('/'); + }); - // 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(req,err); } - }); 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; + // Signup + app.post('/signup', function(req,res,next){ + User.findOne({'email':req.body.email}, function(err,user){ + if (err){ next(err); } - // 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 already exists + else if (user){ + req.flash('warning','A user with that email already exists! If you forgot your password, use this form.'); + 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: '"Trackmap" ', + to: req.body.email, + subject: 'Complete your Trackmap registration', + text: `Welcome to trackmap! \n\nTo complete your registration, follow this link and set your password:\n${env.url}/account/password/${token}`, // plaintext body + html: `

Welcome to trackmap!

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

` // html body + }).then(function(){ + req.flash('success',`An email has been sent to ${req.body.email}. Check your inbox to complete your registration.`); + res.redirect('/'); + }).catch(function(err){ + next(err); + }); }); - })(user.slug, function(newSlug){ - user.slug = newSlug; - if (cbc>1) /* waiting on other calls */ { cbc--; } - else { done(null, user, { success:successMessage, failure:failMessage }); } - }); + + } + }); + }); + + // Forgot password + app.route('/login/forgot') + .all( function(req,res,next){ + if (req.isAuthenticated()){ res.redirect('/account'); } + else { next(); } + }) + .get( function(req,res,next){ + res.render('forgot'); + }) + .post( function(req,res,next){ - // 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 report this error?'; - } else { successMessage = 'Your account has been created. Next maybe you should download the android app. ' } - if (cbc>1) /* waiting on other calls */ { cbc--; } - else { done(null, user, { success:successMessage, failure:failMessage }); } + //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 ${req.body.email} 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: '"Trackmap" ', + to: user.email, + subject: 'Reset your Trackmap password', + text: `Hi, \n\nDid you request to reset your trackmap password? If so, follow this link to do so:\n${env.url}/account/password/${token}\n\nIf you didn't initiate this request, just ignore this email. `, + html: `

Hi,

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

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

` + }).then(function(){ + req.flash('success', `An email has been sent to ${req.body.email}. Check your email for instructions to reset your password. `); + res.redirect('/'); + }).catch(function(err){ + next(err); + }); + }); } }); - - } - - }); - -})); -passport.use(new GoogleTokenStrategy({ - clientID: secret.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); } + }); + + // Social + app.get('/login/:service', function(req,res,next){ + var service = req.params.service; + if (service==='google'){ + var sendParams = {scope:['profile']}; + } + if (!req.user) { // Social login + passport.authenticate(service, sendParams)(req,res,next); + } else if (!req.user.auth[service]) { // Connect social account + passport.authorize(service, sendParams)(req,res,next); + } else { // Disconnect social account + req.user.auth[service] = undefined; + req.user.save(function(err){ + if (err){ return next(err); } + else { + req.flash('success', `${mw.capitalize(service)} account disconnected. `); + res.redirect('/account'); + } }); - return done(err, user); - } else { // No such user - done(null, false); + } }); -})); + app.get('/login/:service/cb', function(req,res,next){ + var service = req.params.service; + if (!req.user) { + passport.authenticate(service, loginOutcome)(req,res,next); + } else { + req.flash('success', `${mw.capitalize(service)} account connected. `); + req.session.returnTo = '/account'; + 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) { + 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(req,err); } +// }); 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 report this error?'; +// } else { successMessage = 'Your account has been created. Next maybe you should download the android app. ' } +// 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); +// } +// }); +// })); diff --git a/config/middleware.js b/config/middleware.js index 1f473f4..eda3794 100644 --- a/config/middleware.js +++ b/config/middleware.js @@ -1,39 +1,36 @@ 'use strict'; -const secret = require('./secrets.js'); - -var throwErr = function(req,err){ - console.error('middleware.js:5 '+typeof err); - console.error('Middleware error:'+err+'\nfor request:\n'+req); - if (secret.env==='production') { - req.flash('danger', 'An error occured.
Would you like to report it?'); - } else { // development - req.flash('danger', err); - } -}; - -var ensureAuth = function(req,res,next){ - if (req.isAuthenticated()) { return next(); } - else { res.redirect('/login'); } -}; - -var ensureAdmin = function(req,res,next){ - ensureAuth(req,res,function(){ - if (req.user.isAdmin){ return next(); } - else { next(); } - //TODO: test this by logging in as !isAdmin and go to /admin - // else if (!res.headersSent) { // 404 to users (not admin) - // var err = new Error('404: Not found: '+req.url); - // err.status = 404; - // res.render('error.html', { - // code: err.status - // }); - // } - }); -}; +const env = require('./env.js'); module.exports = { - throwErr, - ensureAuth, - ensureAdmin + + // Throw error + throwErr: function(req,err){ + console.error('middleware.js:5 '+typeof err); + console.error('Middleware error:'+err+'\nfor request:\n'+req); + if (env.mode==='production') { + req.flash('danger', 'An error occured.
Would you like to report it?'); + } else { // development + req.flash('danger', err); + } + }, + + // Capitalize the first letter of a string + capitalize: function(str){ 'use strict'; + return str.charAt(0).toUpperCase() + str.slice(1); + }, + + // Ensure authentication + ensureAuth: function(req,res,next){ + if (req.isAuthenticated()) { return next(); } + else { res.redirect('/login'); } + }, + + // Ensure administrator + ensureAdmin: function(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 + } + }; \ No newline at end of file diff --git a/config/models/user.js b/config/models/user.js deleted file mode 100644 index 1b7151c..0000000 --- a/config/models/user.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -const mongoose = require('mongoose'); - -const userSchema = new mongoose.Schema({ - name: {type:String, required:true}, - email: String, - slug: {type:String, required:true, unique:true}, - requestId: String, - isAdmin: {type:Boolean, required:true, default:false}, - isPro: {type:Boolean, required:true, default:false}, - created: Date, - lastLogin: Date, - googleID: {type:Number, unique:true}, - 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} - }, - 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} - }, - sk32: {type:String, required:true, unique:true} -}); - -module.exports = mongoose.model('User', userSchema); diff --git a/config/routes/admin.js b/config/routes/admin.js index 2b20322..08b4bac 100644 --- a/config/routes/admin.js +++ b/config/routes/admin.js @@ -2,7 +2,7 @@ const router = require('express').Router(), mw = require('../middleware.js'), - User = require('../models/user.js'); + User = require('../models.js').user; router.route('/') .all(mw.ensureAdmin, function(req,res,next){ @@ -17,7 +17,7 @@ router.route('/') } if (cbc<1){ cbc++; } else { // done - res.render('admin.html', { + res.render('admin', { noFooter: '1', success:req.flash('success')[0], error:req.flash('error')[0] diff --git a/config/routes/auth.js b/config/routes/auth.js deleted file mode 100644 index 1d1c587..0000000 --- a/config/routes/auth.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const router = require('express').Router(), - passport = require('passport'); - -// Routes -router.get('/login', function(req,res){ - res.redirect('/auth/google'); -}); -router.get('/logout', function(req,res){ - req.logout(); // Needs to clear cookies? - req.flash('success', 'You have been logged out. '); - res.redirect('/'); -}); - -// Web app auth -router.get('/auth/google', passport.authenticate('google', { scope: [ - 'https://www.googleapis.com/auth/plus.login', - 'https://www.googleapis.com/auth/plus.profile.emails.read' -] })); -router.get('/auth/google/callback', passport.authenticate('google', { - failureRedirect: '/', - failureFlash: true, - successRedirect: '/', - successFlash: true -} )); - -// Android auth -router.get('/auth/google/idtoken', passport.authenticate('google-id-token'), function (req,res) { - if (!req.user) { res.sendStatus(401); } - else { res.send(req.user); } -} ); - -module.exports = router; \ No newline at end of file diff --git a/config/routes/index.js b/config/routes/index.js index 2f57ccb..a9e09b4 100644 --- a/config/routes/index.js +++ b/config/routes/index.js @@ -3,12 +3,12 @@ const slug = require('slug'), xss = require('xss'), mw = require('../middleware.js'), - User = require('../models/user.js'), + User = require('../models.js').user, router = require('express').Router(); // Index router.get('/', function(req,res,next) { - res.render('index.html'); + res.render('index'); }); // Settings @@ -20,7 +20,7 @@ router.route('/settings').all(mw.ensureAuth, function(req,res,next){ .get(function(req,res,next){ User.findById(req.session.passport.user, function(err,user){ if (err){ console.log('Error finding settings for user:',err); mw.throwErr(req,err); } - res.render('settings.html'); + res.render('settings'); }); }) @@ -70,7 +70,7 @@ router.route('/pro').all(mw.ensureAuth, function(req,res,next){ User.findById(req.session.passport.user, function(err, user){ if (err){ mw.throwErr(req,err); } if (!user){ next(); } - else { res.render('pro.html'); } + else { res.render('pro'); } }); }) @@ -87,13 +87,15 @@ router.route('/pro').all(mw.ensureAuth, function(req,res,next){ }); // Help -router.route('/help').get(mw.ensureAuth, function(req,res){ - res.render('help.html'); +router.get('/help', mw.ensureAuth, function(req,res){ + res.render('help'); }); -// Terms of Service +// Terms of Service and Privacy Policy router.get('/terms', function(req,res){ - res.render('terms.html'); + res.render('terms'); +}).get('/privacy', function(req,res){ + res.render('privacy'); }); module.exports = router; \ No newline at end of file diff --git a/config/routes/map.js b/config/routes/map.js index d45ea09..5e34171 100644 --- a/config/routes/map.js +++ b/config/routes/map.js @@ -2,8 +2,13 @@ const router = require('express').Router(), mw = require('../middleware.js'), - secrets = require('../secrets.js'), - User = require('../models/user.js'); + env = require('../env.js'), + User = require('../models.js').user; + +// Redirect to real slug +router.get('/', mw.ensureAuth, function(req,res){ + res.redirect(`/map/${req.user.slug}`); +}); // Show map router.get('/:slug?', function(req,res,next){ @@ -44,9 +49,9 @@ router.get('/:slug?', function(req,res,next){ res.redirect('/'); } else { if (user && !mapuser) { mapuser = user; } - res.render('map.html', { + res.render('map', { mapuser: mapuser, - mapApi: secrets.googleMapsAPI, + mapApi: env.googleMapsAPI, user: user, noFooter: '1', noHeader: (req.query.noheader)?req.query.noheader.match(/\d/)[0]:'', diff --git a/config/routes/misc.js b/config/routes/misc.js index b1d93da..6b4f252 100644 --- a/config/routes/misc.js +++ b/config/routes/misc.js @@ -2,7 +2,7 @@ const router = require('express').Router(), slug = require('slug'), - User = require('../models/user.js'); + User = require('../models.js').user; // robots.txt router.get('/robots.txt', function(req,res){ diff --git a/config/sockets.js b/config/sockets.js index dd3562b..a8edae0 100644 --- a/config/sockets.js +++ b/config/sockets.js @@ -1,7 +1,7 @@ 'use strict'; // Imports -const User = require('./models/user.js'); +const User = require('./models.js').user; // Check for tracking clients function checkForUsers(io, user) { diff --git a/package.json b/package.json index fe8f59a..bfd128d 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,15 @@ "mongodb": "^2.1.4", "mongoose": "^4.9.0", "node-jose": "^0.8.0", + "nodemailer": "^3.1.8", "nunjucks": "^2.3.0", "passport": "^0.3.2", + "passport-facebook": "^2.1.1", "passport-google-id-token": "^0.4.0", "passport-google-oauth2": "^0.1.6", + "passport-google-oauth20": "^1.0.0", + "passport-local": "^1.0.0", + "passport-twitter": "^1.0.4", "slug": "^0.9.1", "socket.io": "^1.4.4" }, @@ -38,7 +43,6 @@ "test": "mocha test.js", "start": "node server.js", "dev": "nodemon server.js", - "deploy": "ssh khp deploy-tracman", "update": "sudo n stable && sudo npm update --save && sudo npm prune" }, "repository": { diff --git a/server.js b/server.js index 19cc182..4e7e34f 100755 --- a/server.js +++ b/server.js @@ -10,8 +10,8 @@ const nunjucks = require('nunjucks'), passport = require('passport'), flash = require('connect-flash'), - secret = require('./config/secrets.js'), - User = require('./config/models/user.js'), + env = require('./config/env.js'), + User = require('./config/models.js').user, app = express(), http = require('http').Server(app), io = require('socket.io')(http), @@ -19,24 +19,27 @@ const /* SETUP */ { - /* Database */ mongoose.connect(secret.mongoSetup, { + + /* Database */ mongoose.connect(env.mongoSetup, { server:{socketOptions:{ keepAlive:1, connectTimeoutMS:30000 }}, replset:{socketOptions:{ keepAlive:1, connectTimeoutMS:30000 }} }); - /* Templates */ nunjucks.configure(__dirname+'/views', { - autoescape: true, - express: app - }); + /* Templates */ { + nunjucks.configure(__dirname+'/views', { + autoescape: true, + express: app + }); + app.set('view engine','html'); + } /* Session */ { - app.use(cookieParser(secret.cookie)); - // app.use(expressSession({ + app.use(cookieParser(env.cookie)); app.use(cookieSession({ cookie: {maxAge:60000}, - secret: secret.session, + secret: env.session, saveUninitialized: true, resave: true })); @@ -48,18 +51,22 @@ const } /* Auth */ { + require('./config/passport.js')(passport); app.use(passport.initialize()); app.use(passport.session()); - require('./config/auth.js'); - passport.serializeUser(function(user,done) { - done(null, user.id); - }); - passport.deserializeUser(function(id,done) { - User.findById(id, function(err, user) { - if(!err) done(null, user); - else done(err, null); - }); - }); + require('./config/auth.js')(app, passport); + // app.use(passport.initialize()); + // app.use(passport.session()); + // require('./config/auth.js'); + // passport.serializeUser(function(user,done) { + // done(null, user.id); + // }); + // passport.deserializeUser(function(id,done) { + // User.findById(id, function(err, user) { + // if(!err) done(null, user); + // else done(err, null); + // }); + // }); } /* Routes */ { @@ -87,7 +94,6 @@ const // Main routes app.use('/', require('./config/routes/index.js'), - require('./config/routes/auth.js'), require('./config/routes/misc.js') ); @@ -110,11 +116,11 @@ const }); // Handlers - if (secret.env=='production') { + if (env.mode=='production') { app.use(function(err,req,res,next) { if (res.headersSent) { return next(err); } res.status(err.status||500); - res.render('error.html', { + res.render('error', { code: err.status }); }); @@ -124,7 +130,7 @@ const console.log(err); if (res.headersSent) { return next(err); } res.status(err.status||500); - res.render('error.html', { + res.render('error', { code: err.status, message: err.message, error: err @@ -141,11 +147,17 @@ const /* RUNTIME */ { + // Check mail transporter + require('./config/mail.js').verify(function(err, success) { + if (err){ console.error(`SMTP Error: ${err}`); } + console.log(success?'SMTP ready...':'SMTP not ready!'); + }); + // Listen - http.listen(secret.port, function(){ + http.listen(env.port, function(){ console.log( '==========================================\n'+ - 'Listening at '+secret.url+ + 'Listening at '+env.url+ '\n==========================================' ); @@ -158,6 +170,7 @@ const }); }); + } module.exports = app; diff --git a/static/css/base.css b/static/css/base.css index e436145..77b6aab 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -1,105 +1,41 @@ -/* Resets, Clears & Defaults */ -*, *:after, *:before { +/* Global */ +div, footer, .fa, +.container, .container:before, .container:after { box-sizing: border-box; -}::-webkit-scrollbar { - width: 5vw; - min-width:10px; - max-width:40px; -}::-webkit-scrollbar-track { - background-color: #080808; -}::-webkit-scrollbar-thumb { - border-radius: .2vw; - background: #333; -} - -body { - background-color: #080808; - color: #eee; } body, input, textarea { padding: 0; margin: 0; font-family: 'Open Sans', sans-serif; font-size: 18px; color: #eee; - font-weight: 600; } -.flexbox { - width:100%; - display:flex; - justify-content:space-around; +body { + background-color: #080808; } -.flexbox.stretch { justify-content:space-between; } -pre { - white-space: pre-wrap; - white-space: -moz-pre-wrap; - white-space: -pre-wrap; - white-space: -o-pre-wrap; - word-wrap: break-word; +::-webkit-scrollbar { + width: 5vw; + min-width:10px; + max-width:40px; +}::-webkit-scrollbar-track { + background-color: #080808; + background-color: rgba(8,8,8,0); +}::-webkit-scrollbar-thumb { + border-radius: .2vw; + background: #333; } -.dark pre { - -moz-box-shadow: 2px 2px 4px #000; - -webkit-box-shadow: 2px 2px 4px #000; - box-shadow: 2px 2px 4px #000; - background-color: rgba(255,255,255,.03); - color: #aaa; - padding: 1%; - border: 1px solid #ccc; - border-radius: .25rem; +::selection { + background: #999; } -.dark .form-control:disabled, .dark .form-control:disabled { - background-color: rgba(255,255,255,0.1); -} -.input-group { - margin-bottom:30px; -} -input[type="checkbox"] { - margin: 8px 0; -} -.form-group#buttons { - width: 100%; - display: flex; - justify-content: space-around; -} -input[type="checkbox"] { - display: inline-block; -} -.help-block {margin-top:-20px;} - -.alert { - z-index:10; -} -.alert-header { - position: relative; - top: 58px; -} .alert-header.alert-danger { - z-index: 103; -} .alert-header.alert-warning { - z-index: 102; -} .alert-header.alert-success { - z-index: 101; -} -.alert:not(.alert-dismissible) { - text-align: center; -} -.alert a { - color: inherit; - text-decoration: underline; -} -.alert a:hover { - color: inherit; - text-decoration: none; +::-moz-selection { + background: #999; } -input:focus, textarea:focus { - outline: 0; -} - -h1, h2, h3, p { - margin: 0 0 20px 0; +/* Elements */ +h1, h2, h3 { + margin: 0 0 5% 0; position: relative; z-index: 6; } - h1,h2,h3,h4 { font-weight: 600; } h1 { font-size: 48px; @@ -109,27 +45,20 @@ h2 { line-height: 36px; } h3 { font-size: 28px; } h4 { font-size: 20px; } - -.red { color: #fb6e3d; } -.shadow { - -moz-box-shadow: .18vw .18vw .36vw #000; - -webkit-box-shadow: .18vw .18vw .36vw #000; - box-shadow: .18vw .18vw .36vw #000; -} .shadow:active { - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; +p { + margin-top: 0; + margin-bottom: 10vh; } a { color: #fbc93d; text-decoration: none; } -a:hover { +main a:hover:not(.btn) { color: #fbc93d; text-decoration: underline; } -.light a { +.light a:not(.btn) { color:#111; text-decoration: underline; } @@ -138,6 +67,10 @@ a:hover { text-decoration: none; } +hr { + width: 90%; + margin: 10% auto; +} img { max-width: 100%; } @@ -145,27 +78,47 @@ p img { display: block; margin: auto; } - -input[type="checkbox"] { - width: auto; - margin: 8px; -} -.with-errors { - color: #d9534f; +pre { + white-space: pre-wrap; + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + white-space: -o-pre-wrap; + word-wrap: break-word; } -::selection { - background: #999; +.hide { display:none } +.red { color: #fb6e3d; } +.shadow { + -moz-box-shadow: .18vw .18vw .36vw #000; + -webkit-box-shadow: .18vw .18vw .36vw #000; + box-shadow: .18vw .18vw .36vw #000; +} .shadow:active { + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; } -::-moz-selection { - background: #999; -} - -/* End Resets, Clears & Defaults */ - -.container { +.inline { display: inline-block; } +.flex { + width: 100%; + display: flex; + justify-content: space-around; +} +.flex.stretch { justify-content: space-between; } +.left { float: left; } +.right { float: right; } + +main { + top: 60px; + position: absolute; + left: 0px; + right: 0px; + bottom: 0px; + overflow-y: auto; +} +.container { + padding-right: 5%; + padding-left: 5%; width: 100%; - max-width: 1000px; margin: 0 auto; } .container:after { @@ -174,56 +127,122 @@ input[type="checkbox"] { clear: both; } section { - padding: 100px 0 50px; + padding: 10vh 0 5vh; } +/* Alerts */ +.alert { + z-index: 10; + padding: 15px; + border: 1px solid transparent; + border-radius: 4px; +} +.alert a { + z-index: 10; + color: inherit; + font-weight: bold; + text-decoration: underline; +} +.alert a:hover { + color: inherit; + text-decoration: none; +} +.alert h4 { + margin-top: 0; + color: inherit; +} +.alert > p, +.alert > ul { + margin-bottom: 0; +} +.alert > p + p { + margin-top: 5px; +} +.alert-dismissable { + padding-right: 35px; +} +.alert .close, +.alert-dismissible .close { + cursor: pointer; + float: right; + color: inherit; +} +.alert-success { + color: #dff0d8; + background-color: #3c763d; +} +.alert-info { + color: #d9edf7; + background-color: #31708f; +} +.alert-warning { + color: #fcf8e3; + background-color: #8a6d3b; +} +.alert-danger { + color: #f2dede; + background-color: #a94442; +} +.alert.alert-header { + position: relative; + border-radius: 0; + top: 58px; + width: 100%; +} + +/* Buttons */ .btn { text-decoration: none; + text-align: center; font-weight:600; display: inline-block; padding: 15px 30px; - transition: 200ms; - background: transparent; + transition: 100ms; cursor: pointer; - -moz-box-shadow: 2px 2px 4px #000; - -webkit-box-shadow: 2px 2px 4px #000; - box-shadow: 2px 2px 4px #000; -} -.dark .btn { - color: #fff; - border: 1px solid #fff; -} -.dark .btn:hover:not(.disabled), -.dark .btn:active:not(.disabled), -.dark .btn:focus:not(.disabled) { background: rgba(255,255,255,0.1); -}.dark.btn:active:not(.disabled) { - -moz-box-shadow: 0; - -webkit-box-shadow: 0; - box-shadow: 0; -} -.light .btn { - color: #222; + color: #eee; + border: 1px solid #999; + border-radius: .5vw; +} .btn:not(.disabled) { + -moz-box-shadow: + inset .11vw .18vw .52vw rgba(255,255,255,.2), + inset -.11vw -.18vw .52vw rgba(0,0,0,.4), + .11vw .18vw .52vw #000; + -webkit-box-shadow: + inset .11vw .18vw .52vw rgba(255,255,255,.2), + inset -.11vw -.18vw .52vw rgba(0,0,0,.4), + .18vw .18vw .36vw #000; + box-shadow: + inset .11vw .18vw .52vw rgba(255,255,255,.2), + inset -.11vw -.18vw .52vw rgba(0,0,0,.4), + .18vw .18vw .36vw #000; +} .btn:hover:not(.disabled) { text-decoration: none; - border: 1px solid #222; -} -.light .btn:hover:not(.disabled), -.light .btn:active:not(.disabled), -.light .btn:focus:not(.disabled) { - background: rgba(0,0,0,0.1); -} -.btn.yellow { - color: #fbc93d; + background: rgba(255,255,255,0.2); +} .btn:active:not(.disabled) { + -moz-box-shadow: + inset .11vw .18vw .52vw rgba(0,0,0,.4), + inset -.11vw -.18vw .52vw rgba(255,255,255,.2); + -webkit-box-shadow: + inset .11vw .18vw .52vw rgba(0,0,0,.4), + inset -.11vw -.18vw .52vw rgba(255,255,255,.2); + box-shadow: + inset .11vw .18vw .52vw rgba(0,0,0,.4), + inset -.11vw -.18vw .52vw rgba(255,255,255,.2); +} .btn:focus:not(.disabled){ border: 1px solid #fbc93d; } -.dark .btn.yellow:hover:not(.disabled), -.dark .btn.yellow:active:not(.disabled), -.dark .btn.yellow:focus:not(.disabled) { - background: rgba(251,201,61,0.1); -} -.btn.smaller { - padding: 10px 25px; +.btn.main { + color: #fbc93d; } .btn .fa { margin-left: 10px; } + +.group { + width: 100%; +} +.group div { + display: flex; + margin-bottom: 10vh; +} \ No newline at end of file diff --git a/static/css/footer.css b/static/css/footer.css index 5298efe..220f77f 100644 --- a/static/css/footer.css +++ b/static/css/footer.css @@ -1,10 +1,13 @@ footer { font-weight: 300; - width:100%; - overflow:auto; + width: 100%; + overflow: auto; background: #111; color: #ccc; padding: 0 20px; + -moz-box-shadow: inset 0 .25vw 1vw #222; + -webkit-box-shadow: inset 0 .25vw 1vw #222; + box-shadow: inset 0 .25vw 1vw #222; } footer .left { float: left; @@ -33,7 +36,6 @@ footer a .fa { footer .fa a:hover, footer .fa a:focus { color: inherit; } - @media (max-width: 800px) { footer { padding: 0 10px; @@ -49,4 +51,4 @@ footer .fa a:hover, footer .fa a:focus { footer .right { padding-top: 0; } -} +} \ No newline at end of file diff --git a/static/css/header.css b/static/css/header.css index aa63c6a..25b88c2 100644 --- a/static/css/header.css +++ b/static/css/header.css @@ -5,9 +5,10 @@ header { top: 0; left: 0; width: 100%; z-index: 200; +} header a:hover, header a:focus { + color: #fbc93d; } header .logo { - float: left; font-family: 'Open Sans', sans-serif; padding: 13px 23px; @@ -16,53 +17,39 @@ header .logo { font-size: 22px; line-height: 30px; margin: 0; -} -header a:hover, header a:focus { - color: #fbc93d; -} -header .logo a { +} header .logo a { color:inherit; font:inherit; text-decoration:inherit; cursor: pointer; -} -header .logo img { +} header .logo img { margin-right: 10px; - position: relative; - width:28px; - height:28px; + vertical-align: middle; +} header .logo:hover { + text-decoration: none; + background: rgba(255,255,255,0.1); } header nav { float: right; -} -header nav ul { +} header nav ul { padding: 0; margin: 0; -} -header nav ul li { +} header nav ul li { display: inline-block; float: left; -} -header nav ul li a, header nav ul li span { +} header nav ul li a, header nav ul li span { text-decoration:inherit; display: inline-block; padding: 15px 20px; color: #fff; - transition: 200ms; -} -header nav ul li a:hover, + transition: 100ms; +} 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); } -.alert.header { - position: relative; - border-radius: 0; - top: 58px; - width: 100%; -} header .hamburger { display: none; padding: 5px; @@ -70,18 +57,20 @@ header .hamburger { transition-property: opacity, -webkit-filter; transition-property: opacity, filter; transition-property: opacity, filter, -webkit-filter; - transition-duration: 0.15s; - transition-timing-function: linear; } - .hamburger:hover { - opacity: 0.7; } -header .hamburger-box { + transition-duration: 150ms; + transition-timing-function: linear; +} header .hamburger:hover { + opacity: 0.7; +} header .hamburger-box { width: 40px; height: 24px; - position: relative; } -header .hamburger-inner { + position: relative; +} header .hamburger-inner { top: 50%; - margin-top: -2px; } - header .hamburger-inner, header .hamburger-inner::before, header .hamburger-inner::after { + margin-top: -2px; +} header .hamburger-inner, +header .hamburger-inner::before, +header .hamburger-inner::after { width: 40px; height: 4px; background-color: #fff; @@ -90,42 +79,47 @@ header .hamburger-inner { transition-property: -webkit-transform; transition-property: transform; transition-property: transform, -webkit-transform; - transition-duration: 0.15s; - transition-timing-function: ease; } - header .hamburger-inner::before, header .hamburger-inner::after { + transition-duration: 150ms; + transition-timing-function: ease; +} header .hamburger-inner::before, header .hamburger-inner::after { content: ""; - display: block; } - header .hamburger-inner::before { - top: -10px; } - header .hamburger-inner::after { - bottom: -10px; } - -header .hamburger--slider .hamburger-inner { - top: 0; } - header .hamburger--slider .hamburger-inner::before { + display: block; +} header .hamburger-inner::before { + top: -10px; +} header .hamburger-inner::after { + bottom: -10px; + } header .hamburger--slider .hamburger-inner { + top: 0; + } header .hamburger--slider .hamburger-inner::before { top: 10px; transition-property: opacity, -webkit-transform; transition-property: transform, opacity; transition-property: transform, opacity, -webkit-transform; transition-timing-function: ease; - transition-duration: 0.2s; } - header .hamburger--slider .hamburger-inner::after { - top: 20px; } - -header .hamburger--slider.is-active .hamburger-inner { - -webkit-transform: translate3d(0, 10px, 0) rotate(45deg); - transform: translate3d(0, 10px, 0) rotate(45deg); } - header .hamburger--slider.is-active .hamburger-inner::before { + transition-duration: 200ms; + } header .hamburger--slider .hamburger-inner::after { + top: 20px; + } header .hamburger--slider.is-active .hamburger-inner { + -webkit-transform: translate3d(0, 10px, 0) rotate(45deg); + -moz-transform: translate3d(0, 10px, 0) rotate(45deg); + -md-transform: translate3d(0, 10px, 0) rotate(45deg); + -o-transform: translate3d(0, 10px, 0) rotate(45deg); + transform: translate3d(0, 10px, 0) rotate(45deg); + } header .hamburger--slider.is-active .hamburger-inner::before { -webkit-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0); - transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0); - opacity: 0; } - header .hamburger--slider.is-active .hamburger-inner::after { + -moz-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0); + -ms-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0); + -o-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0); + transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0); + opacity: 0; + } header .hamburger--slider.is-active .hamburger-inner::after { -webkit-transform: translate3d(0, -20px, 0) rotate(-90deg); - transform: translate3d(0, -20px, 0) rotate(-90deg); } - - + -moz-transform: translate3d(0, -20px, 0) rotate(-90deg); + -ms-transform: translate3d(0, -20px, 0) rotate(-90deg); + -o-transform: translate3d(0, -20px, 0) rotate(-90deg); + transform: translate3d(0, -20px, 0) rotate(-90deg); + } @media (max-width: 800px) { - header { padding:0; } header nav ul li a { padding:15px; } } @media (max-width: 600px) { @@ -138,17 +132,14 @@ header .hamburger--slider.is-active .hamburger-inner { width: 100%; max-width: 300px; background: #333; - transition: 200ms; - } - header nav.visible { + transition: 100ms; + } header nav.visible { right: 0px; - } - header nav ul li { + } header nav ul li { display: block; float: none; width: 100%; - } - header nav ul li a { + } header nav ul li a { display: block; width: 100%; border-bottom: 1px solid rgba(255,255,255,0.1); @@ -160,4 +151,4 @@ header .hamburger--slider.is-active .hamburger-inner { right: 10px; top: 13px; } -} +} \ No newline at end of file diff --git a/static/js/header.js b/static/js/header.js index 95223c4..d47f5d7 100644 --- a/static/js/header.js +++ b/static/js/header.js @@ -1,3 +1,4 @@ +/* global $ */ 'use strict'; $(document).ready(function(){ @@ -20,4 +21,9 @@ $(document).ready(function(){ $('nav').removeClass('visible'); }); + // Close alerts + $('.alert-dismissible .close').click(function() { + $(this).parent().slideUp(500); + }); + }); diff --git a/views/index.html b/views/index.html index af29ede..63a15fa 100644 --- a/views/index.html +++ b/views/index.html @@ -1,11 +1,11 @@ {% extends 'templates/base.html' %} - {% block head %} {{ super() }} {% endblock %} {% block main %} +
diff --git a/views/login.html b/views/login.html index 30c055c..cf03eca 100644 --- a/views/login.html +++ b/views/login.html @@ -1,38 +1,59 @@ {% extends 'templates/base.html' %} {% block title %}{{ super() }} | Login{% endblock %} +{% block head %} + {{super()}} + + + +{% endblock %} {% block main %} -
+ +
+ +

Welcome!

-
- -

Login

- - - -
+
+ +
-
{% endblock %} \ No newline at end of file diff --git a/views/templates/base.html b/views/templates/base.html index 3e8f4ee..c86fa84 100644 --- a/views/templates/base.html +++ b/views/templates/base.html @@ -5,10 +5,10 @@ {% block title %}Tracman{% endblock %} - + - + @@ -25,14 +25,12 @@ - - - - + + + - - + {% endblock %} {% if not noHeader %}{% endif %} {% if not noFooter %}{% endif %} @@ -40,11 +38,16 @@ {% if not noHeader %}{% include 'templates/header.html' %}{% endif %} - {% block main %}Loading... {% endblock %} - {% if not noFooter %}{% include 'templates/footer.html' %}{% endif %} +
+ {% block main %}{% endblock %} + {% if not noFooter %}{% include 'templates/footer.html' %}{% endif %} +
- + + {% block javascript %} - - - - + {% endblock %} diff --git a/views/templates/header.html b/views/templates/header.html index 5de9750..9163335 100644 --- a/views/templates/header.html +++ b/views/templates/header.html @@ -13,28 +13,25 @@ - +