Added login screen

master
Keith Irwin 2017-04-01 13:03:05 -04:00
parent 76ccaab5c4
commit 7153f7bb9b
No known key found for this signature in database
GPG Key ID: 378933C743E2BBC0
21 changed files with 688 additions and 552 deletions

View File

@ -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.

View File

@ -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

View File

@ -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 <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: '"Trackmap" <accounts@trackmap.tech>',
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: `<p>Welcome to trackmap! </p><p>To complete your registration, follow this link and set your password:<br><a href="${env.url}/account/password/${token}">${env.url}/account/password/${token}</a></p>` // html body
}).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);
});
});
})(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 <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 }); }
//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: '"Trackmap" <accounts@trackmap.tech>',
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: `<p>Hi, </p><p>Did you request to reset your trackmap password? If so, follow this link to do so:<br><a href="${env.url}/account/password/${token}">${env.url}/account/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);
});
});
}
});
}
});
}));
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 <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

@ -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. <br>Would you like to <a href="https://github.com/Tracman-org/Server/issues/new">report it</a>?');
} 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. <br>Would you like to <a href="https://github.com/Tracman-org/Server/issues/new">report it</a>?');
} 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
}
};

View File

@ -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);

View File

@ -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]

View File

@ -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;

View File

@ -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;

View File

@ -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]:'',

View File

@ -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){

View File

@ -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) {

View File

@ -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": {

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
});
});

View File

@ -1,11 +1,11 @@
{% extends 'templates/base.html' %}
{% block head %}
{{ super() }}
<link href="/static/css/index.css" rel="stylesheet">
{% endblock %}
{% block main %}
<script src="/static/js/index.js"></script>
<section class='splash dark' id='splash'>

View File

@ -1,38 +1,59 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | Login{% endblock %}
{% block head %}
{{super()}}
<link rel="stylesheet" type="text/css" href="/static/css/form.css">
<link rel="stylesheet" type="text/css" href="/static/css/login.css">
<style>
p, input, #social-login {
margin-bottom: 5vh;
}
</style>
{% endblock %}
{% block main %}
<section class='dark'>
<section class='container'>
<h1>Welcome!</h1>
<div class='container wrap'>
<h1>Login</h1>
<ul>
<li><a id='google-login' style="cursor:pointer">Google</a></li>
</ul>
</div>
<div class='flex'>
<div class='login'>
<script src="https://www.gstatic.com/firebasejs/3.7.0/firebase.js"></script>
<script>
firebase.initializeApp({
apiKey: "AIzaSyDPYY_Fw3FXLm0hKfIfc8qlrc98zZiN4IY",
authDomain: "tracman-b894f.firebaseapp.com",
databaseURL: "https://tracman-b894f.firebaseio.com",
storageBucket: "tracman-b894f.appspot.com",
messagingSenderId: "483494341936"
});
$('#google-login').click(function() {
var googleProvider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(googleProvider).then(function(result) {
console.log(`Successfully logged in ${result.user}`);
}).catch(function(error) {
console.error(error.message);
});
});
<h3>Login</h3>
<form method="post">
<div id='social-login' class='flex'>
<a href="/login/google" class='gp btn'><i class="fa fa-google-plus"></i></a>
<a href="/login/facebook" class='fb btn'><i class="fa fa-facebook"></i></a>
<a href="/login/twitter" class='tw btn'><i class="fa fa-twitter"></i></a>
</div>
<input type="email" placeholder="Email" name="email" required>
<input type="password" placeholder="Password" name="password" required>
<input type="submit" value="Sign in" class='btn main'>
<p><a href="/login/forgot">Forgot your password?</a></p>
</form>
</div>
<hr class='hide'>
<div class='signup'>
<h3>Create account</h3>
<p>Welcome aboard! </p>
<form action="/signup" method="POST">
<input type="email" name="email" placeholder="Your Email" required>
<p>You will be sent an email confrimation with a link to create a password. </p>
<p>By signing up, you agree to our <a href="/terms">terms of service</a> and <a href="/privacy">privacy policy</a>. </p>
<input type="submit" value="Sign up" class='btn'>
</form>
</div>
</div>
</script>
</section>
</section>
{% endblock %}

View File

@ -5,10 +5,10 @@
<title>{% block title %}Tracman{% endblock %}</title>
<link rel="manifest" href="/static/manifest.webmanifest">
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<meta http-equiv="Content-type" content="text/html;charset=utf-8">
<meta charset="UTF-8">
<meta name="author" content="Keith Irwin">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
<meta name="keywords" content="map, phone, gps, link, location, track, friends, app">
<meta name="description" content="Tracman lets you see and share your phone's exact realtime location">
<meta name="theme-color" content="#222">
@ -25,14 +25,12 @@
<link rel="icon apple-touch-icon" sizes="228x228" type="image/png" href="/static/img/icon/by/228.png">
<link rel="apple-touch-icon-precomposed" type="image/png" href="/static/img/icon/by/152.png">
<link rel="stylesheet" type="text/css" href="/static/css/bootstrap.css">
<link href="/static/css/base.css" rel="stylesheet">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,600">
<link rel="stylesheet" type="text/css" href="/static/css/base.css">
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Open+Sans:300,600">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Merriweather:300,700">
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
{% endblock %}
{% if not noHeader %}<link href="/static/css/header.css" rel="stylesheet">{% endif %}
{% if not noFooter %}<link href="/static/css/footer.css" rel="stylesheet">{% endif %}
@ -40,11 +38,16 @@
<body>
{% if not noHeader %}{% include 'templates/header.html' %}{% endif %}
{% block main %}Loading... {% endblock %}
{% if not noFooter %}{% include 'templates/footer.html' %}{% endif %}
<main>
{% block main %}{% endblock %}
{% if not noFooter %}{% include 'templates/footer.html' %}{% endif %}
</main>
<!-- Google Analytics -->
<!-- Javascript -->
{% block javascript %}
<script>
// Google analytics
(function(t,r,a,c,m,o,n){t['GoogleAnalyticsObject']=m;t[m]=t[m]||function(){
(t[m].q=t[m].q||[]).push(arguments);},t[m].l=1*new Date();o=r.createElement(a),
n=r.getElementsByTagName(a)[0];o.async=1;o.src=c;n.parentNode.insertBefore(o,n);
@ -53,19 +56,9 @@
ga('create','UA-44266909-3','auto');
ga('require','linkid');
ga('send','pageview');
</script>
<!-- Firebase -->
<script src="https://www.gstatic.com/firebasejs/3.7.0/firebase.js"></script>
<script>
firebase.initializeApp({
apiKey: "AIzaSyDPYY_Fw3FXLm0hKfIfc8qlrc98zZiN4IY",
authDomain: "tracman-b894f.firebaseapp.com",
databaseURL: "https://tracman-b894f.firebaseio.com",
storageBucket: "tracman-b894f.appspot.com",
messagingSenderId: "483494341936"
});
</script>
{% endblock %}
</body>
</html>

View File

@ -13,28 +13,25 @@
</div>
<!-- Navigation -->
<nav id='navigation'>
<ul>
{% if user %}
<li><a href="/map/{{user.slug}}">Map</a></li>
<li><a href="/settings">Settings</a></li>
{% if user.isAdmin %}<li><a href="/admin">Admin</a></li>{% endif %}
<li><a href="/help">Help</a></li>
<li><a href="/logout">Logout</a></li>
{% else %}
<li><a href="/#overview">About</a></li>
<li><a href="/map/keith">Demo</a></li>
<li><a href="/#join">Join</a></li>
<li><a href="/login">Login</a></li>
{% endif %}
</ul>
</nav>
<nav id='navigation'><ul>
{% if user %}
<li><a href="/map">Map</a></li>
<li><a href="/settings">Settings</a></li>
{% if user.isAdmin %}<li><a href="/admin">Admin</a></li>{% endif %}
<li><a href="/help">Help</a></li>
<li><a href="/logout">Logout</a></li>
{% else %}
<li><a href="/">About</a></li>
<li><a href="/map/keith">Demo</a></li>
<li><a href="/login">Login</a></li>
{% endif %}
</ul></nav>
</header>
<!-- Flash messages -->
<noscript>
<div class='alert alert-header alert-danger alert-dismissible shadow'>
<div class='alert alert-header alert-danger alert-dismissible shadow'>
<strong>Uh-oh!</strong> You don't have javascript enabled! This page won't load correctly without it. You should really enable it, because many websites won't work properly. Ask your grandchildren if you need help.
<a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a>
</div>