Merged hotfix-0.6.4 into master
commit
6effafaf2a
|
@ -1,5 +1,9 @@
|
|||
# Tracman Server Changelog
|
||||
###### v 0.6.3
|
||||
###### v 0.6.4
|
||||
|
||||
#### v0.6.4
|
||||
|
||||
* [#92](https://github.com/Tracman-org/Server/issues/92) Fixed blank map issue
|
||||
|
||||
#### v0.6.3
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# <img align="left" src="/static/img/icon/by/48.png" alt="[]" title="The Tracman Logo">Tracman
|
||||
###### v 0.6.3
|
||||
###### v 0.6.4
|
||||
|
||||
node.js application to display a sharable map with user's location.
|
||||
|
||||
|
@ -52,6 +52,9 @@ Tracman will be updated according to [this branching model](http://nvie.com/post
|
|||
|
||||
[view full changelog](CHANGELOG.md)
|
||||
|
||||
#### v0.6.4
|
||||
|
||||
* [#92](https://github.com/Tracman-org/Server/issues/92) Fixed blank map issue
|
||||
|
||||
#### v0.6.3
|
||||
|
||||
|
|
|
@ -12,7 +12,9 @@ module.exports = {
|
|||
// Location of your mongoDB
|
||||
mongoSetup: 'mongodb://localhost:27017/tracman',
|
||||
// Or use the test database from mLab
|
||||
//mongoSetup: 'mongodb://tracman:MUPSLXQ34f9cQTc5@ds113841.mlab.com:13841/tracman-dev',
|
||||
//mongoSetup: 'mongodb://tracman:MUPSLXQ34f9cQTc5@ds133961.mlab.com:33961/tracman',
|
||||
// You can log in there with:
|
||||
// mongo ds133961.mlab.com:33961/tracman-dev -u contributor -p opensourcerules
|
||||
|
||||
// URL and port where this will run
|
||||
url: 'https://localhost:8080',
|
||||
|
@ -29,4 +31,8 @@ module.exports = {
|
|||
// Google maps API key
|
||||
googleMapsAPI: 'XXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXX'
|
||||
|
||||
// reCaptcha API key
|
||||
recaptchaSitekey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
recaptchaSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
|
||||
};
|
||||
|
|
|
@ -34,7 +34,7 @@ module.exports = {
|
|||
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>`,
|
||||
noReply: `"Tracman" <NoReply@tracman.org>`,
|
||||
|
||||
to: (user)=>{
|
||||
return `"${user.name}" <${user.email}>`;
|
||||
|
|
|
@ -29,7 +29,7 @@ module.exports = {
|
|||
|
||||
// Ensure administrator
|
||||
ensureAdmin: (req,res,next)=>{
|
||||
if (req.user.isAdmin){ return next(); }
|
||||
if (req.isAuthenticated() && req.user.isAdmin){ return next(); }
|
||||
else {
|
||||
let err = new Error("Unauthorized");
|
||||
err.status = 401;
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
const mongoose = require('mongoose'),
|
||||
unique = require('mongoose-unique-validator'),
|
||||
bcrypt = require('bcrypt'),
|
||||
crypto = require('crypto');
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('tracman-models');
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
name: {type:String},
|
||||
|
@ -15,9 +16,9 @@ const userSchema = new mongoose.Schema({
|
|||
password: String,
|
||||
passToken: String,
|
||||
passTokenExpires: Date,
|
||||
google: {type:String, unique:true},
|
||||
facebook: {type:String, unique:true},
|
||||
twitter: {type:String, unique:true},
|
||||
google: String,
|
||||
facebook: String,
|
||||
twitter: String,
|
||||
},
|
||||
isAdmin: {type:Boolean, required:true, default:false},
|
||||
isPro: {type:Boolean, required:true, default:false},
|
||||
|
@ -52,13 +53,13 @@ const userSchema = new mongoose.Schema({
|
|||
|
||||
// Create email confirmation token
|
||||
userSchema.methods.createEmailToken = function(next){ // next(err,token)
|
||||
//console.log('user.createEmailToken() called');
|
||||
debug('user.createEmailToken() called');
|
||||
var user = this;
|
||||
|
||||
crypto.randomBytes(16, (err,buf)=>{
|
||||
if (err){ next(err,null); }
|
||||
if (buf){
|
||||
//console.log(`Buffer ${buf.toString('hex')} created`);
|
||||
debug(`Buffer ${buf.toString('hex')} created`);
|
||||
user.emailToken = buf.toString('hex');
|
||||
user.save()
|
||||
.then( ()=>{
|
||||
|
@ -79,7 +80,7 @@ const userSchema = new mongoose.Schema({
|
|||
|
||||
// Reuse old token, resetting clock
|
||||
if ( user.auth.passTokenExpires >= Date.now() ){
|
||||
//console.log(`Reusing old password token...`);
|
||||
debug(`Reusing old password token...`);
|
||||
user.auth.passTokenExpires = Date.now() + 3600000; // 1 hour
|
||||
user.save()
|
||||
.then( ()=>{
|
||||
|
@ -92,7 +93,7 @@ const userSchema = new mongoose.Schema({
|
|||
|
||||
// Create new token
|
||||
else {
|
||||
//console.log(`Creating new password token...`);
|
||||
debug(`Creating new password token...`);
|
||||
crypto.randomBytes(16, (err,buf)=>{
|
||||
if (err){ return next(err,null,null); }
|
||||
if (buf) {
|
||||
|
@ -100,9 +101,11 @@ const userSchema = new mongoose.Schema({
|
|||
user.auth.passTokenExpires = Date.now() + 3600000; // 1 hour
|
||||
user.save()
|
||||
.then( ()=>{
|
||||
debug('successfully saved user in createPassToken');
|
||||
return next(null,user.auth.passToken,user.auth.passTokenExpires);
|
||||
})
|
||||
.catch( (err)=>{
|
||||
debug('error saving user in createPassToken');
|
||||
return next(err,null,null);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ const
|
|||
GoogleTokenStrategy = require('passport-google-id-token'),
|
||||
FacebookTokenStrategy = require('passport-facebook-token'),
|
||||
TwitterTokenStrategy = require('passport-twitter-token'),
|
||||
debug = require('debug')('tracman-passport'),
|
||||
env = require('./env/env.js'),
|
||||
mw = require('./middleware.js'),
|
||||
User = require('./models.js').user;
|
||||
|
@ -31,7 +32,7 @@ module.exports = (passport)=>{
|
|||
passwordField: 'password',
|
||||
passReqToCallback: true
|
||||
}, (req,email,password,done)=>{
|
||||
//console.log(`Perfoming local login for ${email}`);
|
||||
debug(`Perfoming local login for ${email}`);
|
||||
User.findOne({'email':email})
|
||||
.then( (user)=>{
|
||||
|
||||
|
@ -73,13 +74,13 @@ module.exports = (passport)=>{
|
|||
|
||||
// Social login
|
||||
function socialLogin(req, service, profileId, done) {
|
||||
//console.log(`socialLogin() called`);
|
||||
debug(`socialLogin() called`);
|
||||
let query = {};
|
||||
query['auth.'+service] = profileId;
|
||||
|
||||
// Intent to log in
|
||||
if (!req.user) {
|
||||
//console.log(`Logging in with ${service}...`);
|
||||
debug(`Logging in with ${service}...`);
|
||||
User.findOne(query)
|
||||
.then( (user)=>{
|
||||
|
||||
|
@ -110,8 +111,7 @@ module.exports = (passport)=>{
|
|||
|
||||
// No such user
|
||||
else {
|
||||
req.session.flashType = 'warning';
|
||||
req.session.flashMessage = `There's no user for that ${service} account. `;
|
||||
req.flash('warning', `There's no user for that ${service} account. `);
|
||||
return done();
|
||||
}
|
||||
|
||||
|
@ -124,16 +124,15 @@ module.exports = (passport)=>{
|
|||
|
||||
// No googleId either
|
||||
else {
|
||||
//console.log(`Couldn't find ${service} user.`);
|
||||
req.session.flashType = 'warning';
|
||||
req.session.flashMessage = `There's no user for that ${service} account. `;
|
||||
debug(`Couldn't find ${service} user.`);
|
||||
req.flash('warning', `There's no user for that ${service} account. `);
|
||||
return done();
|
||||
}
|
||||
}
|
||||
|
||||
// Successfull social login
|
||||
else {
|
||||
//console.log(`Found user: ${user}`);
|
||||
debug(`Found user: ${user}`);
|
||||
req.session.flashType = 'success';
|
||||
req.session.flashMessage = "You have been logged in.";
|
||||
return done(null, user);
|
||||
|
@ -148,7 +147,7 @@ module.exports = (passport)=>{
|
|||
|
||||
// Intent to connect account
|
||||
else {
|
||||
//console.log(`Attempting to connect ${service} account...`);
|
||||
debug(`Attempting to connect ${service} account...`);
|
||||
|
||||
// Check for unique profileId
|
||||
User.findOne(query)
|
||||
|
@ -156,7 +155,7 @@ module.exports = (passport)=>{
|
|||
|
||||
// Social account already in use
|
||||
if (existingUser) {
|
||||
//console.log(`${service} account already in use.`);
|
||||
debug(`${service} account already in use.`);
|
||||
req.session.flashType = 'warning';
|
||||
req.session.flashMessage = `Another user is already connected to that ${service} account. `;
|
||||
return done();
|
||||
|
@ -164,7 +163,7 @@ module.exports = (passport)=>{
|
|||
|
||||
// Connect to account
|
||||
else {
|
||||
//console.log(`Connecting ${service} account.`);
|
||||
debug(`Connecting ${service} account.`);
|
||||
req.user.auth[service] = profileId;
|
||||
req.user.save()
|
||||
.then( ()=>{
|
||||
|
|
|
@ -4,41 +4,32 @@ const router = require('express').Router(),
|
|||
mw = require('../middleware.js'),
|
||||
User = require('../models.js').user;
|
||||
|
||||
router.route('/')
|
||||
.all(mw.ensureAdmin, (req,res,next)=>{
|
||||
next();
|
||||
} )
|
||||
|
||||
.get( (req,res)=>{
|
||||
router.get('/', mw.ensureAdmin, (req,res)=>{
|
||||
|
||||
User.find({}).sort({lastLogin:-1})
|
||||
.then( (found)=>{
|
||||
res.render('admin', {
|
||||
noFooter: '1',
|
||||
users: found
|
||||
users: found,
|
||||
total: found.length
|
||||
});
|
||||
})
|
||||
.catch( (err)=>{ mw.throwErr(err,req); });
|
||||
|
||||
} )
|
||||
});
|
||||
|
||||
.post( (req,res,next)=>{
|
||||
if (req.body.delete) {
|
||||
User.findOneAndRemove({'_id':req.body.delete})
|
||||
router.get('/delete/:usrid', mw.ensureAdmin, (req,res,next)=>{
|
||||
|
||||
User.findOneAndRemove({'_id':req.params.usrid})
|
||||
.then( (user)=>{
|
||||
req.flash('success', '<i>'+user.name+'</i> deleted.');
|
||||
res.redirect('/admin#users');
|
||||
res.redirect('/admin');
|
||||
})
|
||||
.catch( (err)=>{
|
||||
mw.throwErr(err,req);
|
||||
res.redirect('/admin#users');
|
||||
res.redirect('/admin');
|
||||
});
|
||||
}
|
||||
else {
|
||||
let err = new Error('POST without action sent. ');
|
||||
err.status = 500;
|
||||
next();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -7,6 +7,7 @@ const
|
|||
crypto = require('crypto'),
|
||||
moment = require('moment'),
|
||||
slugify = require('slug'),
|
||||
debug = require('debug')('tracman-routes-auth'),
|
||||
env = require('../env/env.js');
|
||||
|
||||
module.exports = (app, passport) => {
|
||||
|
@ -18,14 +19,14 @@ module.exports = (app, passport) => {
|
|||
failureFlash: true
|
||||
},
|
||||
loginCallback = (req,res)=>{
|
||||
//console.log(`Login callback called... redirecting to ${req.session.next}`);
|
||||
debug(`Login callback called... redirecting to ${req.session.next}`);
|
||||
req.flash(req.session.flashType,req.session.flashMessage);
|
||||
req.session.flashType = undefined;
|
||||
req.session.flashMessage = undefined;
|
||||
res.redirect( req.session.next || '/map' );
|
||||
},
|
||||
appLoginCallback = (req,res,next)=>{
|
||||
//console.log('appLoginCallback called.');
|
||||
debug('appLoginCallback called.');
|
||||
if (req.user){ res.send(req.user); }
|
||||
else {
|
||||
let err = new Error("Unauthorized");
|
||||
|
@ -61,10 +62,12 @@ module.exports = (app, passport) => {
|
|||
|
||||
// Send token and alert user
|
||||
function sendToken(user){
|
||||
debug('sendToken(user)');
|
||||
|
||||
// Create a password token
|
||||
user.createPassToken( (err,token,expires)=>{
|
||||
if (err){
|
||||
debug('Error creating password token');
|
||||
mw.throwErr(err,req);
|
||||
res.redirect('/login#signup');
|
||||
}
|
||||
|
@ -77,7 +80,7 @@ module.exports = (app, passport) => {
|
|||
|
||||
// Email the instructions to continue
|
||||
mail.send({
|
||||
from: mail.from,
|
||||
from: mail.noReply,
|
||||
to: `<${user.email}>`,
|
||||
subject: 'Complete your Tracman registration',
|
||||
text: mail.text(`Welcome to Tracman! \n\nTo complete your registration, follow this link and set your password:\n${env.url}/settings/password/${token}\n\nThis link will expire at ${expirationTimeString}. `),
|
||||
|
@ -137,6 +140,7 @@ module.exports = (app, passport) => {
|
|||
if (existingUser){
|
||||
crypto.randomBytes(6, (err,buf)=>{
|
||||
if (err) {
|
||||
debug('Failed to create random bytest for slug');
|
||||
mw.throwErr(err,req);
|
||||
reject();
|
||||
}
|
||||
|
@ -151,11 +155,13 @@ module.exports = (app, passport) => {
|
|||
|
||||
})
|
||||
.catch((err)=>{
|
||||
debug('Failed to create slug');
|
||||
mw.throwErr(err,req);
|
||||
reject();
|
||||
});
|
||||
|
||||
})(user.slug, (newSlug)=>{
|
||||
debug('Successfully created slug');
|
||||
user.slug = newSlug;
|
||||
resolve();
|
||||
});
|
||||
|
@ -163,13 +169,16 @@ module.exports = (app, passport) => {
|
|||
|
||||
// Generate sk32
|
||||
const sk32 = new Promise((resolve,reject) => {
|
||||
debug('Creating sk32');
|
||||
crypto.randomBytes(32, (err,buf)=>{
|
||||
if (err) {
|
||||
debug('Failed to create sk32');
|
||||
mw.throwErr(err,req);
|
||||
reject();
|
||||
}
|
||||
if (buf) {
|
||||
user.sk32 = buf.toString('hex');
|
||||
debug('Successfully created sk32');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
@ -177,9 +186,10 @@ module.exports = (app, passport) => {
|
|||
|
||||
// Save user and send the token by email
|
||||
Promise.all([slug, sk32])
|
||||
.then( ()=>{ user.save(); })
|
||||
// .then( ()=>{ user.save(); })
|
||||
.then( ()=>{ sendToken(user); })
|
||||
.catch( (err)=>{
|
||||
debug('Failed to save user');
|
||||
mw.throwErr(err,req);
|
||||
res.redirect('/login#signup');
|
||||
});
|
||||
|
@ -228,7 +238,7 @@ module.exports = (app, passport) => {
|
|||
|
||||
// Email reset link
|
||||
mail.send({
|
||||
from: mail.from,
|
||||
from: mail.noReply,
|
||||
to: mail.to(user),
|
||||
subject: 'Reset your Tracman password',
|
||||
text: mail.text(`Hi, \n\nDid you request to reset your Tracman password? If so, follow this link to do so:\n${env.url}/settings/password/${token}\n\nIf you didn't initiate this request, just ignore this email. `),
|
||||
|
@ -267,19 +277,19 @@ module.exports = (app, passport) => {
|
|||
|
||||
// Social login
|
||||
if (!req.user) {
|
||||
//console.log(`Attempting to login with ${service} with params: ${JSON.stringify(sendParams)}...`);
|
||||
debug(`Attempting to login with ${service} with params: ${JSON.stringify(sendParams)}...`);
|
||||
passport.authenticate(service, sendParams)(req,res,next);
|
||||
}
|
||||
|
||||
// Connect social account
|
||||
else if (!req.user.auth[service]) {
|
||||
//console.log(`Attempting to connect ${service} account...`);
|
||||
debug(`Attempting to connect ${service} account...`);
|
||||
passport.authorize(service, sendParams)(req,res,next);
|
||||
}
|
||||
|
||||
// Disconnect social account
|
||||
else {
|
||||
//console.log(`Attempting to disconnect ${service} account...`);
|
||||
debug(`Attempting to disconnect ${service} account...`);
|
||||
|
||||
// Make sure the user has a password before they disconnect their google login account
|
||||
// This is because login used to only be through google, and some people might not have
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const mw = require('../middleware.js'),
|
||||
env = require('../env/env.js'),
|
||||
mail = require('../mail.js'),
|
||||
router = require('express').Router(),
|
||||
request = require('request'),
|
||||
slug = require('slug'),
|
||||
xss = require('xss'),
|
||||
User = require('../models.js').user;
|
||||
|
@ -18,6 +21,68 @@ module.exports = router
|
|||
res.render('help');
|
||||
})
|
||||
|
||||
// Contact
|
||||
.get('/contact', (req,res)=>{
|
||||
res.render('contact',{
|
||||
sitekey: env.recaptchaSitekey
|
||||
});
|
||||
})
|
||||
.post('/contact', (req,res,next)=>{
|
||||
|
||||
// Confirm captcha
|
||||
request.post( 'https://www.google.com/recaptcha/api/siteverify', {form:{
|
||||
secret: env.recaptchaSecret,
|
||||
response: req.body['g-recaptcha-response'],
|
||||
remoteip: req.ip
|
||||
}}, (err, response, body)=>{
|
||||
|
||||
// Check for errors
|
||||
if (err){
|
||||
mw.throwErr(err,req);
|
||||
res.redirect('/contact');
|
||||
}
|
||||
if (response.statusCode!==200) {
|
||||
let err = new Error('Bad response from reCaptcha service');
|
||||
mw.throwErr(err,req);
|
||||
res.redirect('/contact');
|
||||
}
|
||||
else {
|
||||
|
||||
// Captcha succeeded
|
||||
if (JSON.parse(body).success){
|
||||
mail.send({
|
||||
from: `${req.body.name} <${req.body.email}>`,
|
||||
to: `Tracman Contact <contact@tracman.org>`,
|
||||
subject: req.body.subject||'A message',
|
||||
text: req.body.message
|
||||
})
|
||||
.then(()=>{
|
||||
req.flash('success', `Your message has been sent. `);
|
||||
res.redirect(req.session.next || '/');
|
||||
})
|
||||
.catch((err)=>{
|
||||
mw.throwErr(err,req);
|
||||
res.redirect('/contact');
|
||||
});
|
||||
}
|
||||
|
||||
// Captcha failed
|
||||
else {
|
||||
let err = new Error('Failed reCaptcha');
|
||||
mw.throwErr(err,req);
|
||||
res.redirect('/contact');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
//TODO: Check req.body.g-recaptcha-response
|
||||
|
||||
|
||||
|
||||
})
|
||||
|
||||
// Terms of Service and Privacy Policy
|
||||
.get('/terms', (req,res)=>{
|
||||
res.render('terms');
|
||||
|
|
|
@ -5,9 +5,15 @@ const router = require('express').Router(),
|
|||
env = require('../env/env.js'),
|
||||
User = require('../models.js').user;
|
||||
|
||||
|
||||
// Redirect to real slug
|
||||
router.get('/', mw.ensureAuth, (req,res)=>{
|
||||
if (req.query.new){
|
||||
res.redirect(`/map/${req.user.slug}?new=1`);
|
||||
}
|
||||
else {
|
||||
res.redirect(`/map/${req.user.slug}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Show map
|
||||
|
@ -23,7 +29,8 @@ router.get('/:slug?', (req,res,next)=>{
|
|||
user: req.user,
|
||||
noFooter: '1',
|
||||
noHeader: (req.query.noheader)?req.query.noheader.match(/\d/)[0]:0,
|
||||
disp: (req.query.disp)?req.query.disp.match(/\d/)[0]:2 // 0=map, 1=streetview, 2=both
|
||||
disp: (req.query.disp)?req.query.disp.match(/\d/)[0]:2, // 0=map, 1=streetview, 2=both
|
||||
newuserurl: (req.query.new)? env.url+'/map/'+req.params.slug : ''
|
||||
});
|
||||
}
|
||||
}).catch( (err)=>{
|
||||
|
|
|
@ -8,6 +8,7 @@ const slug = require('slug'),
|
|||
User = require('../models.js').user,
|
||||
mail = require('../mail.js'),
|
||||
env = require('../env/env.js'),
|
||||
debug = require('debug')('tracman-settings'),
|
||||
router = require('express').Router();
|
||||
|
||||
// Validate email addresses
|
||||
|
@ -51,26 +52,26 @@ router.route('/')
|
|||
|
||||
// Not unique!
|
||||
if (existingUser && existingUser.id!==req.user.id) {
|
||||
//console.log("Email not unique!");
|
||||
debug("Email not unique!");
|
||||
req.flash('warning', `That email, <u>${req.body.email}</u>, is already in use by another user! `);
|
||||
resolve();
|
||||
}
|
||||
|
||||
// It's unique
|
||||
else {
|
||||
//console.log("Email is unique");
|
||||
debug("Email is unique");
|
||||
req.user.newEmail = req.body.email;
|
||||
|
||||
// Create token
|
||||
//console.log(`Creating email token...`);
|
||||
debug(`Creating email token...`);
|
||||
req.user.createEmailToken((err,token)=>{
|
||||
if (err){ reject(err); }
|
||||
|
||||
// Send token to user by email
|
||||
//console.log(`Mailing new email token to ${req.body.email}...`);
|
||||
debug(`Mailing new email token to ${req.body.email}...`);
|
||||
mail.send({
|
||||
to: `"${req.user.name}" <${req.body.email}>`,
|
||||
from: mail.from,
|
||||
from: mail.noReply,
|
||||
subject: 'Confirm your new email address for Tracman',
|
||||
text: mail.text(`A request has been made to change your Tracman email address. If you did not initiate this request, please disregard it. \n\nTo confirm your email, follow this link:\n${env.url}/settings/email/${token}. `),
|
||||
html: mail.html(`<p>A request has been made to change your Tracman email address. If you did not initiate this request, please disregard it. </p><p>To confirm your email, follow this link:<br><a href="${env.url}/settings/email/${token}">${env.url}/settings/email/${token}</a>. </p>`)
|
||||
|
@ -132,7 +133,7 @@ router.route('/')
|
|||
// Set settings when done
|
||||
Promise.all([checkEmail, checkSlug])
|
||||
.then( ()=>{
|
||||
//console.log('Setting settings... ');
|
||||
debug('Setting settings... ');
|
||||
|
||||
// Set values
|
||||
req.user.name = xss(req.body.name);
|
||||
|
@ -147,10 +148,10 @@ router.route('/')
|
|||
};
|
||||
|
||||
// Save user and send response
|
||||
//console.log(`Saving new settings for user ${req.user.name}...`);
|
||||
debug(`Saving new settings for user ${req.user.name}...`);
|
||||
req.user.save()
|
||||
.then( ()=>{
|
||||
//console.log(`DONE! Redirecting user...`);
|
||||
debug(`DONE! Redirecting user...`);
|
||||
req.flash('success', 'Settings updated. ');
|
||||
res.redirect('/settings');
|
||||
})
|
||||
|
@ -165,11 +166,10 @@ router.route('/')
|
|||
res.redirect('/settings');
|
||||
});
|
||||
|
||||
} )
|
||||
|
||||
// Delete user account
|
||||
.delete( (req,res,next)=>{
|
||||
} );
|
||||
|
||||
// Delete account
|
||||
router.get('/delete', (req,res)=>{
|
||||
User.findByIdAndRemove(req.user)
|
||||
.then( ()=>{
|
||||
req.flash('success', 'Your account has been deleted. ');
|
||||
|
@ -179,7 +179,6 @@ router.route('/')
|
|||
mw.throwErr(err,req);
|
||||
res.redirect('/settings');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Confirm email address
|
||||
|
@ -242,7 +241,7 @@ router.route('/password')
|
|||
// Confirm password change request by email.
|
||||
mail.send({
|
||||
to: mail.to(req.user),
|
||||
from: mail.from,
|
||||
from: mail.noReply,
|
||||
subject: 'Request to change your Tracman password',
|
||||
text: mail.text(`A request has been made to change your tracman password. If you did not initiate this request, please contact support at keith@tracman.org. \n\nTo change your password, follow this link:\n${env.url}/settings/password/${token}. \n\nThis request will expire at ${expirationTimeString}. `),
|
||||
html: mail.html(`<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 at ${expirationTimeString}. </p>`)
|
||||
|
@ -266,14 +265,17 @@ router.route('/password/:token')
|
|||
|
||||
// Check token
|
||||
.all( (req,res,next)=>{
|
||||
debug('/settings/password/:token .all() called');
|
||||
User
|
||||
.findOne({'auth.passToken': req.params.token})
|
||||
.where('auth.passTokenExpires').gt(Date.now())
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
debug('Bad token');
|
||||
req.flash('danger', 'Password reset token is invalid or has expired. ');
|
||||
res.redirect( (req.isAuthenticated)?'/settings':'/login' );
|
||||
} else {
|
||||
debug('setting passwordUser');
|
||||
res.locals.passwordUser = user;
|
||||
next();
|
||||
}
|
||||
|
@ -286,6 +288,7 @@ router.route('/password/:token')
|
|||
|
||||
// Show password change form
|
||||
.get( (req,res)=>{
|
||||
debug('/settings/password/:token .get() called');
|
||||
res.render('password');
|
||||
} )
|
||||
|
||||
|
@ -313,10 +316,11 @@ router.route('/password/:token')
|
|||
req.flash('success', 'Your password has been changed. ');
|
||||
res.redirect('/settings');
|
||||
}
|
||||
|
||||
// New user created password
|
||||
else {
|
||||
req.flash('success', 'Password set. You can use it to log in now. ');
|
||||
res.redirect('/login?next=/settings');
|
||||
res.redirect('/login?next=/map?new=1');
|
||||
}
|
||||
|
||||
} );
|
||||
|
|
|
@ -10,7 +10,7 @@ router
|
|||
.get('/mail', (req,res,next)=>{
|
||||
mail.send({
|
||||
to: `"Keith Irwin" <hypergeek14@gmail.com>`,
|
||||
from: mail.from,
|
||||
from: mail.noReply,
|
||||
subject: 'Test email',
|
||||
text: mail.text("Looks like everything's working! "),
|
||||
html: mail.html("<p>Looks like everything's working! </p>")
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
// Imports
|
||||
const User = require('./models.js').user;
|
||||
const debug = require('debug')('tracman-sockets'),
|
||||
User = require('./models.js').user;
|
||||
|
||||
// Check for tracking clients
|
||||
function checkForUsers(io, user) {
|
||||
//console.log(`Checking for clients receiving updates for ${user}`);
|
||||
debug(`Checking for clients receiving updates for ${user}`);
|
||||
|
||||
// Checks if any sockets are getting updates for this user
|
||||
if (Object.values(io.sockets.connected).some( (socket)=>{
|
||||
return socket.gets===user;
|
||||
})) {
|
||||
//console.log(`Activating updates for ${user}.`);
|
||||
debug(`Activating updates for ${user}.`);
|
||||
io.to(user).emit('activate','true');
|
||||
} else {
|
||||
//console.log(`Deactivating updates for ${user}.`);
|
||||
debug(`Deactivating updates for ${user}.`);
|
||||
io.to(user).emit('activate', 'false');
|
||||
}
|
||||
}
|
||||
|
@ -25,22 +26,22 @@ module.exports = {
|
|||
|
||||
init: (io)=>{
|
||||
io.on('connection', (socket)=>{
|
||||
//console.log(`${socket.id} connected.`);
|
||||
debug(`${socket.id} connected.`);
|
||||
|
||||
// Set a few variables
|
||||
//socket.ip = socket.client.request.headers['x-real-ip'];
|
||||
//socket.ua = socket.client.request.headers['user-agent'];
|
||||
|
||||
/* Log */
|
||||
//socket.on('log', (text)=>{
|
||||
//console.log(`LOG: ${text}`);
|
||||
//});
|
||||
socket.on('log', (text)=>{
|
||||
debug(`LOG: ${text}`);
|
||||
});
|
||||
|
||||
// This socket can set location (app)
|
||||
socket.on('can-set', (userId)=>{
|
||||
//console.log(`${socket.id} can set updates for ${userId}.`);
|
||||
debug(`${socket.id} can set updates for ${userId}.`);
|
||||
socket.join(userId, ()=>{
|
||||
//console.log(`${socket.id} joined ${userId}`);
|
||||
debug(`${socket.id} joined ${userId}`);
|
||||
});
|
||||
checkForUsers( io, userId );
|
||||
});
|
||||
|
@ -48,17 +49,21 @@ module.exports = {
|
|||
// This socket can receive location (map)
|
||||
socket.on('can-get', (userId)=>{
|
||||
socket.gets = userId;
|
||||
//console.log(`${socket.id} can get updates for ${userId}.`);
|
||||
debug(`${socket.id} can get updates for ${userId}.`);
|
||||
socket.join(userId, ()=>{
|
||||
//console.log(`${socket.id} joined ${userId}`);
|
||||
debug(`${socket.id} joined ${userId}`);
|
||||
socket.to(userId).emit('activate', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
// Set location
|
||||
socket.on('set', (loc)=>{
|
||||
//console.log(`${socket.id} set location for ${loc.usr}`);
|
||||
loc.time = Date.now();
|
||||
debug(`${socket.id} set location for ${loc.usr}`);
|
||||
debug(`Location was set to: ${JSON.stringify(loc)}`);
|
||||
|
||||
// Get android timestamp or use server timestamp
|
||||
if (loc.ts){ loc.tim = Date(loc.ts); }
|
||||
else { loc.tim = Date.now(); }
|
||||
|
||||
// Check for user and sk32 token
|
||||
if (!loc.usr){
|
||||
|
@ -80,7 +85,7 @@ module.exports = {
|
|||
|
||||
// Broadcast location
|
||||
io.to(loc.usr).emit('get', loc);
|
||||
//console.log(`Broadcasting ${loc.lat}, ${loc.lon} to ${loc.usr}`);
|
||||
debug(`Broadcasting ${loc.lat}, ${loc.lon} to ${loc.usr}`);
|
||||
|
||||
// Save in db as last seen
|
||||
user.last = {
|
||||
|
@ -88,7 +93,7 @@ module.exports = {
|
|||
lon: parseFloat(loc.lon),
|
||||
dir: parseFloat(loc.dir||0),
|
||||
spd: parseFloat(loc.spd||0),
|
||||
time: loc.time
|
||||
time: loc.tim
|
||||
};
|
||||
user.save()
|
||||
.catch( (err)=>{ console.error("❌", err.stack); });
|
||||
|
@ -102,11 +107,11 @@ module.exports = {
|
|||
|
||||
// Shutdown (check for remaining clients)
|
||||
socket.on('disconnect', (reason)=>{
|
||||
//console.log(`${socket.id} disconnected because of a ${reason}.`);
|
||||
debug(`${socket.id} disconnected because of a ${reason}.`);
|
||||
|
||||
// Check if client was receiving updates
|
||||
if (socket.gets){
|
||||
//console.log(`${socket.id} left ${socket.gets}`);
|
||||
debug(`${socket.id} left ${socket.gets}`);
|
||||
checkForUsers( io, socket.gets );
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tracman",
|
||||
"version": "0.6.3",
|
||||
"version": "0.6.4",
|
||||
"description": "Tracks user's GPS location",
|
||||
"main": "server.js",
|
||||
"dependencies": {
|
||||
|
@ -9,6 +9,7 @@
|
|||
"connect-flash-plus": "^0.2.1",
|
||||
"cookie-parser": "^1.4.1",
|
||||
"cookie-session": "^2.0.0-alpha.1",
|
||||
"debug": "^2.6.6",
|
||||
"express": "^4.15.2",
|
||||
"express-validator": "^3.1.3",
|
||||
"kerberos": "0.0.17",
|
||||
|
@ -28,6 +29,7 @@
|
|||
"passport-local": "^1.0.0",
|
||||
"passport-twitter": "^1.0.4",
|
||||
"passport-twitter-token": "^1.3.0",
|
||||
"request": "^2.81.0",
|
||||
"slug": "^0.9.1",
|
||||
"socket.io": "^1.4.4",
|
||||
"xss": "^0.3.3"
|
||||
|
|
|
@ -7,6 +7,7 @@ const
|
|||
expressValidator = require('express-validator'),
|
||||
cookieParser = require('cookie-parser'),
|
||||
cookieSession = require('cookie-session'),
|
||||
debug = require('debug')('tracman-server'),
|
||||
mongoose = require('mongoose'),
|
||||
nunjucks = require('nunjucks'),
|
||||
passport = require('passport'),
|
||||
|
@ -78,9 +79,9 @@ const
|
|||
|
||||
// Path for redirects
|
||||
let nextPath = ((req.query.next)?req.query.next: req.path.substring(0,req.path.indexOf('#')) || req.path );
|
||||
if ( nextPath.substring(0,6)!=='/login' && nextPath.substring(0,7)!=='/logout' ){
|
||||
if ( nextPath.substring(0,6)!=='/login' && nextPath.substring(0,7)!=='/logout' && nextPath.substring(0,7)!=='/static' ){
|
||||
req.session.next = nextPath+'#';
|
||||
//console.log(`Set redirect path to ${nextPath}#`);
|
||||
debug(`Set redirect path to ${nextPath}#`);
|
||||
}
|
||||
|
||||
// User account
|
||||
|
@ -130,7 +131,7 @@ const
|
|||
// Production handlers
|
||||
if (env.mode!=='development') {
|
||||
app.use( (err,req,res,next)=>{
|
||||
if (err.status!==404){ console.error(`❌ ${err.stack}`); }
|
||||
if (err.status!==404&&err.status!==401){ console.error(`❌ ${err.stack}`); }
|
||||
if (res.headersSent) { return next(err); }
|
||||
res.status(err.status||500);
|
||||
res.render('error', {
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
input, textarea {
|
||||
margin-bottom: 3%;
|
||||
}
|
||||
|
||||
#subject, #message {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
#name, #email {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@media (min-width:600px) {
|
||||
#name, #email {
|
||||
min-width: 45%;
|
||||
}
|
||||
#name {
|
||||
float: left;
|
||||
}
|
||||
#email {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
button.btn {
|
||||
display: block;
|
||||
margin: auto;
|
||||
min-width: 60%;
|
||||
min-height: 12vh;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
thead > tr:nth-child() {
|
||||
background: #333333;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background: #111111;
|
||||
}
|
||||
tr:nth-child(odd) {
|
||||
background: #181818;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 1%;
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -15,6 +15,7 @@ $(function(){
|
|||
// Success callback
|
||||
function(pos){
|
||||
var newloc = {
|
||||
ts: Date.now(),
|
||||
tok: token,
|
||||
usr: userid,
|
||||
lat: pos.coords.latitude,
|
||||
|
@ -54,6 +55,7 @@ $(function(){
|
|||
// Success callback
|
||||
function(pos) {
|
||||
newloc = {
|
||||
ts: Date.now(),
|
||||
tok: token,
|
||||
usr: userid,
|
||||
lat: pos.coords.latitude,
|
||||
|
@ -96,13 +98,14 @@ $(function(){
|
|||
else {
|
||||
// Stop tracking
|
||||
if (wpid) {
|
||||
$('#track-loc').html('<i class="fa fa-crosshairs"></i> Track');
|
||||
$('#track-loc').html('<i class="fa fa-crosshairs"></i>Track');
|
||||
navigator.geolocation.clearWatch(wpid);
|
||||
wpid = undefined;
|
||||
}
|
||||
|
||||
// Clear location
|
||||
newloc = {
|
||||
ts: Date.now(),
|
||||
tok: token,
|
||||
usr: userid,
|
||||
lat:0, lon:0, spd:0
|
||||
|
|
|
@ -51,7 +51,7 @@ function parseLoc(loc) {
|
|||
loc.dir = parseFloat(loc.dir);
|
||||
loc.lat = parseFloat(loc.lat);
|
||||
loc.lon = parseFloat(loc.lon);
|
||||
loc.time = new Date(loc.time).toLocaleString();
|
||||
loc.tim = new Date(loc.tim).toLocaleString();
|
||||
loc.glatlng = new google.maps.LatLng(loc.lat, loc.lon);
|
||||
return loc;
|
||||
}
|
||||
|
@ -62,7 +62,8 @@ function toggleMaps(loc) {
|
|||
$('#map').hide();
|
||||
$('#pano').hide();
|
||||
$('#notset').show();
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
$('#map').show();
|
||||
$('#pano').show();
|
||||
$('#notset').hide();
|
||||
|
@ -83,6 +84,7 @@ window.gmapsCb = function() {
|
|||
// Create map
|
||||
if (disp!=='1') {
|
||||
//console.log("Creating map...");
|
||||
|
||||
map = new google.maps.Map( mapElem, {
|
||||
center: new google.maps.LatLng( mapuser.last.lat, mapuser.last.lon ),
|
||||
panControl: false,
|
||||
|
@ -190,12 +192,10 @@ socket.on('get', function(loc) {
|
|||
if (disp!=='1') {
|
||||
|
||||
// Update time
|
||||
$('#timestamp').text('location updated '+loc.time);
|
||||
|
||||
// Show or hide map
|
||||
toggleMaps(loc);
|
||||
$('#timestamp').text('location updated '+loc.tim);
|
||||
|
||||
// Update marker and map center
|
||||
google.maps.event.trigger(map,'resize');
|
||||
map.setCenter({ lat:loc.lat, lng:loc.lon });
|
||||
marker.setPosition({ lat:loc.lat, lng:loc.lon });
|
||||
|
||||
|
|
|
@ -26,16 +26,7 @@ $(function(){
|
|||
// Delete account
|
||||
$('#delete').click(function(){
|
||||
if (confirm("Are you sure you want to delete your account? This CANNOT be undone! ")) {
|
||||
$.ajax({
|
||||
url: '/settings',
|
||||
type: 'DELETE',
|
||||
success: function(){
|
||||
location.reload();
|
||||
},
|
||||
fail: function(){
|
||||
alert("Failed to delete account!");
|
||||
}
|
||||
});
|
||||
window.location.href = "/settings/delete";
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -3,27 +3,31 @@
|
|||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
.container { max-width:90%; }
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/.table.min.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<section class='dark'>
|
||||
<div class='container' id='tabs'>
|
||||
<section class='container'>
|
||||
|
||||
<div id='users'>
|
||||
<h1>Users</h1>
|
||||
<table id='users-table' class='table table-hover'>
|
||||
|
||||
<h1 class='left'>Users</h1>
|
||||
<div id='stat' class='right'>
|
||||
<p><b>Total</b>: {{total}}</p>
|
||||
</div>
|
||||
|
||||
<table id='users-table'>
|
||||
|
||||
<thead><tr>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th>Joined</th>
|
||||
<th>Last login</th>
|
||||
<th>Moved</th>
|
||||
<th>Accounts</th>
|
||||
<th>Social</th>
|
||||
<th>Edit</th>
|
||||
</tr></thead>
|
||||
|
||||
<tbody>
|
||||
{% for usr in users %}
|
||||
<tr>
|
||||
|
@ -33,23 +37,22 @@
|
|||
<td id='{{usr.id}}-logged'></td>
|
||||
<td id='{{usr.id}}-moved'></td>
|
||||
<td id='{{usr.id}}-accounts'>
|
||||
{% if usr.googleID %}
|
||||
<a href="https://plus.google.com/{{usr.googleID}}/">Google</a>
|
||||
{% endif %}
|
||||
{% if usr.auth.google %}<a href="https://plus.google.com/{{usr.auth.google}}/">G</a>{% endif %}
|
||||
{% if usr.auth.facebook %}<a href="https://facebook.com/{{usr.auth.facebook}}/">F</a>{% endif %}
|
||||
{% if usr.auth.twitter %}<a href="https://twitter.com/{{usr.auth.twitter}}/">T</a>{% endif %}
|
||||
</td>
|
||||
<td id='{{usr.id}}-edit'>
|
||||
<a class='btn' href="/delete/{{usr.id}}">DELETE</a>
|
||||
</td>
|
||||
<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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="application/javascript" src="/static/js/.moment.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js" integrity="sha256-1hjUhpc44NwiNg8OwMu2QzJXhD8kcj+sJA3aCQZoUjg=" crossorigin="anonymous"></script>
|
||||
<script type="application/javascript">
|
||||
|
||||
/* DATE/TIME FORMATS */ {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
{% block title %}{{super()}} | Contact{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{super()}}
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/.form.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/.contact.min.css">
|
||||
<script src='https://www.google.com/recaptcha/api.js'></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<section class='container'>
|
||||
|
||||
<h1>Contact</h1>
|
||||
|
||||
<form id='contact-form' role="form" method="POST">
|
||||
|
||||
<input name="subject" id='subject' type="text" maxlength="160" value="{{subject}}" placeholder="Subject">
|
||||
<textarea name="message" id='message' rows="10" maxlength="5000" placeholder="Message" required>{{message}}</textarea>
|
||||
|
||||
<input name="name" id='name' type="text" maxlength="160" value="{{name}}" placeholder="Name">
|
||||
<input name="email" id='email' type="email" maxlength="160" value="{{email}}" placeholder="Email" required>
|
||||
|
||||
<button class='g-recaptcha main btn' data-sitekey="{{sitekey}}" data-callback="onSubmit">Submit</button>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
{{super()}}
|
||||
<script type="application/javascript">
|
||||
/* global $ */
|
||||
function onSubmit() {
|
||||
$('#contact-form').submit();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,11 +1,12 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
{% block title %}{{super()}} | {% if code %}{{code}} {% endif %}Error{% endblock %}
|
||||
{% block title %}{{super()}} | {% if code %}{{code}}{% endif %}{% if message %}: {{message}}{% else %} Error{% endif%}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<section class='container'>
|
||||
{% if message %}<h2>❌️ {{message}}</h2>{% endif %}
|
||||
{% if stack %}<p>{{stack}}</p>{% else %}
|
||||
{% if code == '404' %}<p>This page does not exist. Maybe you followed a dead link here. </p>
|
||||
{% elif code == '401' %}<p>You aren't allowed in here. Shoo. </p>
|
||||
{% else %}<p>Would you please <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 %}{% endif %}
|
||||
</section>
|
||||
|
|
|
@ -44,6 +44,38 @@
|
|||
|
||||
{% block main %}
|
||||
|
||||
{% if user and newuserurl %}
|
||||
<div id='welcome'>
|
||||
<style>
|
||||
#welcome {
|
||||
background: #111;
|
||||
padding: 3vw;
|
||||
border-radius: 2vw;
|
||||
z-index: 50;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
#welcome h1 {
|
||||
display: inline-block;
|
||||
}
|
||||
#welcome p {
|
||||
margin: 0;
|
||||
}
|
||||
#welcome .close {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h1>Welcome!</h1>
|
||||
<span class='close' onclick="$('#welcome').hide();">✖️</span>
|
||||
<p>This is your map. It's avaliable at <a href="{{newuserurl}}">{{newuserurl}}</a>. You can change that and other settings in <b><a href="/settings">settings</a></b>. Set your location by clicking <b>set</b> below. Clear it by clicking <b>clear</b>. To track your location, click <b>track</b> or download the <a href="/android">android app</a>. For more information, see <a href="/help">the help page</a>. </p>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id='map'></div>
|
||||
<div id='pano'></div>
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<!-- Navigation -->
|
||||
<nav id='navigation'><ul>
|
||||
<li><a href="/">About</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
{% if user %}
|
||||
<li><a href="/map">Map</a></li>
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
|
|
Loading…
Reference in New Issue