Merge pull request #71 from Tracman-org/release-0.6.0

Release 0.6.0
master
Keith Irwin 2017-04-28 00:38:40 -07:00 committed by GitHub
commit 6865ac52a0
59 changed files with 13959 additions and 7934 deletions

4
.gitignore vendored
View File

@ -2,8 +2,8 @@
node_modules
# Secret stuff
config/env*
!config/env-sample.js
config/env/*
!config/env/sample.js
# Minified static files (can be built with `npm run minify`)
static/**/*.min.*

View File

@ -200,42 +200,3 @@ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY C
### 17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
## END OF TERMS AND CONDITIONS
#### How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>.

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

@ -1,26 +1,58 @@
# Tracman
###### v 0.5.1
# <img align="left" src="/static/img/icon/by/48.png" alt="[]" title="The Tracman Logo">Tracman
###### v 0.6.0
node.js application to display a map with user's location.
node.js application to display a sharable map with user's location.
## Installation
```sh
$ git clone https://github.com/Tracman-org/Server.git && (cd Server && exec npm install)
```
## Running
You will need to set up a configuration file at `config/env/env.js`. Use `config/env/sample.js` for an example. 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).
A good method is to simply copy the sample configuration and point `config/env/env.js` to the new version:
```sh
$ npm start
$ cp config/env/sample.js config/env/my-config.js
$ echo "module.exports = require('./my-config.js');" > config/env/env.js
```
Then edit `config/env/my-config.js` to match your local environment.
## Usage
Run Tracman with npm:
```sh
$ npm run minify && npm start
```
...or with [nodemon](https://nodemon.io/):
```sh
$ npm run nodemon
```
Nodemon will automatically minify files and restart the app when you make changes. Check out the `nodemon.json` configuration.
## Contributing
Tracman will be updated according to [this branching model](http://nvie.com/posts/a-successful-git-branching-model).
Tracman will be updated according to [this branching model](http://nvie.com/posts/a-successful-git-branching-model)... more or less. If you know anything about programming Android, [the Tracman android app](https://github.com/Tracman-org/Android) is more desperate for help.
## Changelog
#### v 0.5.1
#### v0.6.0
* [#32](https://github.com/Tracman-org/Server/issues/32), [#57](https://github.com/Tracman-org/Server/issues/57), [#58](https://github.com/Tracman-org/Server/issues/58), [#60](https://github.com/Tracman-org/Server/issues/60) Added more login options
* [#50](https://github.com/Tracman-org/Server/issues/50) Replaced some callbacks with promises
* Minified static files
* [#51](https://github.com/Tracman-org/Server/issues/51), [#52](https://github.com/Tracman-org/Server/issues/52) Added settings validations
* [#54](https://github.com/Tracman-org/Server/issues/54), [#55](https://github.com/Tracman-org/Server/issues/55) Made map work better
* [#61](https://github.com/Tracman-org/Server/issues/61) New MongoDB security
* [#62](https://github.com/Tracman-org/Server/issues/62) Fixed error handling
#### v0.5.1
* Fixed broken controls
@ -28,7 +60,7 @@ Tracman will be updated according to [this branching model](http://nvie.com/post
* Updated libraries
* Fixed recognition of attached clients [#34](https://github.com/Tracman-org/Server/issues/21)
* Moved socket.io code to own file.
* Moved socket.io code to own file.
* Many minor fixes
#### v0.4.3
@ -56,4 +88,17 @@ Tracman will be updated according to [this branching model](http://nvie.com/post
* Unified map and dashboard UI
* Security updates
* New admin UI
*
## License
###### see [LICENSE.md](https://github.com/Tracman-org/Server/blob/master/LICENSE.md)
Tracman: GPS tracking service in node.js
Copyright © 2017 [Keith Irwin](https://keithirwin.us/)
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <[http://www.gnu.org/licenses/](http://www.gnu.org/licenses/)>.

View File

@ -1,108 +0,0 @@
'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');
passport.use(new GoogleStrategy({
clientID: secret.googleClientId,
clientSecret: secret.googleClientSecret,
callbackURL: secret.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: 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); }
});
return done(err, user);
} else { // No such user
done(null, false);
}
});
}));

32
config/env/sample.js vendored Normal file
View File

@ -0,0 +1,32 @@
'use strict';
module.exports = {
// Local variables
mode: 'development', // or production
// Random strings to prevent hijacking
session: 'SomeSecret',
cookie: 'SomeOtherSecret',
// Location of your mongoDB
mongoSetup: 'mongodb://localhost:27017/tracman',
// Or use the test database from mLab
//mongoSetup: 'mongodb://tracman:MUPSLXQ34f9cQTc5@ds113841.mlab.com:13841/tracman-dev',
// URL and port where this will run
url: 'https://localhost:8080',
port: 8080,
// OAuth API keys
facebookAppId: 'XXXXXXXXXXXXXXXX',
facebookAppSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
twitterConsumerKey: 'XXXXXXXXXXXXXXXXXXXXXXXXX',
twitterConsumerSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
googleClientId: '############-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com',
googleClientSecret: 'XXXXXXXXX_XXXXXXXXXXXXXX',
// Google maps API key
googleMapsAPI: 'XXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXX'
};

43
config/mail.js Normal file
View File

@ -0,0 +1,43 @@
'use strict';
const nodemailer = require('nodemailer'),
env = require('./env/env.js');
let transporter = nodemailer.createTransport({
host: 'keithirwin.us',
port: 587,
secure: false,
requireTLS: true,
auth: {
user: 'NoReply@tracman.org',
pass: 'Ei0UwfrZuE'
},
// logger: true,
// debug: true
});
/* Confirm login */
// transporter.verify( (err,success)=>{
// if (err){ console.error(`SMTP Error: ${err}`); }
// console.log(`SMTP ${!success?'not ':''}ready...`);
// } );
module.exports = {
send: transporter.sendMail.bind(transporter),
text: (text)=>{
return `Tracman\n\n${text}\n\nDo not reply to this email\nFor information about why you received this email, see the privacy policy at ${env.url}/privacyy#email`;
},
html: (text)=>{
return `<h1><a href="/" style="text-decoration:none;"><span style="color:#000;font-family:sans-serif;font-size:36px;font-weight:bold"><img src="${env.url}/static/img/icon/by/32.png" alt="+" style="margin-right:10px">Tracman</span></a></h1>${text}<p style="font-size:8px;">Do not reply to this email. For information about why you recieved this email, see our <a href="${env.url}/privacy#email">privacy policy</a>. </p>`;
},
from: `"Tracman" <NoReply@tracman.org>`,
to: (user)=>{
return `"${user.name}" <${user.email}>`;
}
};

View File

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

133
config/models.js Normal file
View File

@ -0,0 +1,133 @@
'use strict';
const mongoose = require('mongoose'),
unique = require('mongoose-unique-validator'),
bcrypt = require('bcrypt-nodejs'),
crypto = require('crypto');
const userSchema = new mongoose.Schema({
name: {type:String},
email: {type:String, unique:true},
newEmail: String,
emailToken: String,
slug: {type:String, required:true, unique:true},
auth: {
password: String,
passToken: String,
passTokenExpires: Date,
google: {type:String, unique:true},
facebook: {type:String, unique:true},
twitter: {type:String, unique:true},
},
isAdmin: {type:Boolean, required:true, default:false},
isPro: {type:Boolean, required:true, default:false},
created: {type:Date, required:true},
lastLogin: Date,
settings: {
units: {type:String, default:'standard'},
defaultMap: {type:String, default:'road'},
defaultZoom: {type:Number, default:11},
showScale: {type:Boolean, default:false},
showSpeed: {type:Boolean, default:false},
showTemp: {type:Boolean, default:false},
showAlt: {type:Boolean, default:false},
showStreetview: {type:Boolean, default:false}
},
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}
}).plugin(unique);
/* User methods */ {
//TODO: Return promises instead of taking callbacks
// See https://gist.github.com/7h1b0/5154fda207e68ad1cefc#file-random-js
// For an example
// Create email confirmation token
userSchema.methods.createEmailToken = function(next){ // next(err,token)
//console.log('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`);
user.emailToken = buf.toString('hex');
user.save()
.then( ()=>{
return next(null,user.emailToken);
})
.catch( (err)=>{
return next(err,null);
});
}
});
};
// Create password reset token
userSchema.methods.createPassToken = function(next){ // next(err,token,expires)
var user = this;
// Reuse old token, resetting clock
if ( user.auth.passTokenExpires >= Date.now() ){
console.log(`Reusing old password token...`);
user.auth.passTokenExpires = Date.now() + 3600000; // 1 hour
user.save()
.then( ()=>{
return next(null,user.auth.passToken,user.auth.passTokenExpires);
})
.catch( (err)=>{
return next(err,null,null);
});
}
// Create new token
else {
console.log(`Creating new password token...`);
crypto.randomBytes(16, (err,buf)=>{
if (err){ return next(err,null,null); }
if (buf) {
user.auth.passToken = buf.toString('hex');
user.auth.passTokenExpires = Date.now() + 3600000; // 1 hour
user.save()
.then( ()=>{
return next(null,user.auth.passToken,user.auth.passTokenExpires);
})
.catch( (err)=>{
return next(err,null,null);
});
}
});
}
};
// Generate hash for new password
userSchema.methods.generateHash = function(password,next){
// next(err,hash);
bcrypt.genSalt(8)
.then( (salt)=>{
bcrypt.hash(password, salt, null, next);
})
.catch( (err)=>{ return next(err,null); });
};
// Check for valid password
userSchema.methods.validPassword = function(password,next){
bcrypt.compare(password, this.auth.password, next);
};
}
module.exports = {
'user': mongoose.model('User', userSchema)
};

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

244
config/passport.js Normal file
View File

@ -0,0 +1,244 @@
'use strict';
const
LocalStrategy = require('passport-local').Strategy,
GoogleStrategy = require('passport-google-oauth20').Strategy,
FacebookStrategy = require('passport-facebook').Strategy,
TwitterStrategy = require('passport-twitter').Strategy,
GoogleTokenStrategy = require('passport-google-id-token'),
FacebookTokenStrategy = require('passport-facebook-token'),
TwitterTokenStrategy = require('passport-twitter-token'),
env = require('./env/env.js'),
mw = require('./middleware.js'),
User = require('./models.js').user;
module.exports = (passport)=>{
// Serialize/deserialize users
passport.serializeUser((user,done)=>{
done(null, user.id);
});
passport.deserializeUser((id,done)=>{
User.findById(id, (err,user)=>{
if(!err){ done(null, user); }
else { done(err, null); }
});
});
// Local
passport.use('local', new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
}, (req,email,password,done)=>{
//console.log(`Perfoming local login for ${email}`);
User.findOne({'email':email})
.then( (user)=>{
// No user with that email
if (!user) {
req.session.next = undefined;
return done( null, false, req.flash('warning','Incorrect email or password.') );
}
// User exists
else {
// Check password
user.validPassword( password, (err,res)=>{
if (err){ return done(err); }
// Password incorrect
if (!res) {
req.session.next = undefined;
return done( null, false, req.flash('warning','Incorrect email or password.') );
}
// Successful login
else {
user.lastLogin = Date.now();
user.save();
return done(null,user);
}
} );
}
})
.catch( (err)=>{
return done(err);
});
}
));
// Social login
function socialLogin(req, service, profileId, done) {
//console.log(`socialLogin() called`);
let query = {};
query['auth.'+service] = profileId;
// Intent to log in
if (!req.user) {
//console.log(`Logging in with ${service}...`);
User.findOne(query)
.then( (user)=>{
// Can't find user
if (!user){
// Lazy update from old googleId field
if (service==='google') {
User.findOne({ 'googleID': parseInt(profileId) })
.then( (user)=>{
// User exists with old schema
if (user) {
user.auth.google = profileId;
user.googleId = undefined;
user.save()
.then( ()=>{
console.info(`🗂️ Lazily updated schema for ${user.name}.`);
req.session.flashType = 'success';
req.session.flashMessage = "You have been logged in. ";
return done(null, user);
})
.catch( (err)=>{
mw.throwErr(err,req);
return done(err);
});
}
// No such user
else {
req.session.flashType = 'warning';
req.session.flashMessage = `There's no user for that ${service} account. `;
return done();
}
})
.catch ( (err)=>{
mw.throwErr(err,req);
return done(err);
});
}
// 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. `;
return done();
}
}
// Successfull social login
else {
//console.log(`Found user: ${user}`);
req.session.flashType = 'success';
req.session.flashMessage = "You have been logged in.";
return done(null, user);
}
})
.catch( (err)=>{
mw.throwErr(err,req);
return done(err);
});
}
// Intent to connect account
else {
//console.log(`Attempting to connect ${service} account...`);
// Check for unique profileId
User.findOne(query)
.then( (existingUser)=>{
// Social account already in use
if (existingUser) {
//console.log(`${service} account already in use.`);
req.session.flashType = 'warning';
req.session.flashMessage = `Another user is already connected to that ${service} account. `;
return done();
}
// Connect to account
else {
//console.log(`Connecting ${service} account.`);
req.user.auth[service] = profileId;
req.user.save()
.then( ()=>{
req.session.flashType = 'success';
req.session.flashMessage = `${mw.capitalize(service)} account connected. `;
return done(null,req.user);
} )
.catch( (err)=>{
return done(err);
} );
}
})
.catch( (err)=>{
mw.throwErr(err,req);
return done(err);
});
}
}
// Google
passport.use('google', new GoogleStrategy({
clientID: env.googleClientId,
clientSecret: env.googleClientSecret,
callbackURL: env.url+'/login/google/cb',
passReqToCallback: true
}, (req, accessToken, refreshToken, profile, done)=>{
socialLogin(req, 'google', profile.id, done);
}
)).use('google-token', new GoogleTokenStrategy({
clientID: env.googleClientId,
passReqToCallback: true
}, (req, parsedToken, googleId, done)=>{
socialLogin(req,'google', googleId, done);
}
));
// Facebook
passport.use('facebook', new FacebookStrategy({
clientID: env.facebookAppId,
clientSecret: env.facebookAppSecret,
callbackURL: env.url+'/login/facebook/cb',
passReqToCallback: true
}, (req, accessToken, refreshToken, profile, done)=>{
socialLogin(req, 'facebook', profile.id, done);
}
)).use('facebook-token', new FacebookTokenStrategy({
clientID: env.facebookAppId,
clientSecret: env.facebookAppSecret,
passReqToCallback: true
}, (req, accessToken, refreshToken, profile, done)=>{
socialLogin(req,'facebook', profile.id, done);
}
));
// Twitter
passport.use(new TwitterStrategy({
consumerKey: env.twitterConsumerKey,
consumerSecret: env.twitterConsumerSecret,
callbackURL: env.url+'/login/twitter/cb',
passReqToCallback: true
}, (req, token, tokenSecret, profile, done)=>{
socialLogin(req, 'twitter', profile.id, done);
}
)).use('twitter-token', new TwitterTokenStrategy({
consumerKey: env.twitterConsumerKey,
consumerSecret: env.twitterConsumerSecret,
passReqToCallback: true
}, (req, token, tokenSecret, profile, done)=>{
socialLogin(req,'twitter', profile.id, done);
}
));
return passport;
};

View File

@ -2,52 +2,43 @@
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){
.all(mw.ensureAdmin, (req,res,next)=>{
next();
}).get(function(req,res){
var cbc = 0;
var checkCBC = function(req,res,err){
if (err) {
req.flash('error', err.message);
console.log(err);
}
if (cbc<1){ cbc++; }
else { // done
res.render('admin.html', {
noFooter: '1',
success:req.flash('success')[0],
error:req.flash('error')[0]
});
}
};
User.findById(req.session.passport.user, function(err, found) {
res.locals.user = found;
checkCBC(req,res,err);
});
User.find({}).sort({lastLogin:-1}).exec(function(err, found){
res.locals.users = found;
checkCBC(req,res,err);
});
});
} )
router.route('/users')
.all(mw.ensureAdmin, function(req,res,next){
next();
}).post(function(req,res,next){
.get( (req,res)=>{
User.find({}).sort({lastLogin:-1})
.then( (found)=>{
res.render('admin', {
noFooter: '1',
users: found
});
})
.catch( (err)=>{ mw.throwErr(err,req); });
} )
.post( (req,res,next)=>{
if (req.body.delete) {
User.findOneAndRemove({'_id':req.body.delete}, function(err,user){
if (err){ req.flash('error', err.message); }
else { req.flash('success', '<i>'+user.name+'</i> deleted.'); }
User.findOneAndRemove({'_id':req.body.delete})
.then( (user)=>{
req.flash('success', '<i>'+user.name+'</i> deleted.');
res.redirect('/admin#users');
})
.catch( (err)=>{
mw.throwErr(err,req);
res.redirect('/admin#users');
});
} else { console.log('ERROR! POST without action sent. '); next(); }
});
}
else {
let err = new Error('POST without action sent. ');
err.status = 500;
next();
}
} );
module.exports = router;

View File

@ -1,30 +1,287 @@
'use strict';
const router = require('express').Router(),
passport = require('passport');
router.get('/login', function(req,res){
res.redirect('/auth/google');
});
router.get('/logout', function(req,res){
req.logout(); // Needs to clear cookies?
res.redirect('/');
});
const
mw = require('../middleware.js'),
mail = require('../mail.js'),
User = require('../models.js').user,
crypto = require('crypto'),
env = require('../env/env.js');
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
} ));
module.exports = (app, passport) => {
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;
// Methods for success and failure
const
loginOutcome = {
failureRedirect: '/login',
failureFlash: true
},
loginCallback = (req,res)=>{
//console.log(`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.');
if (req.user){ res.send(req.user); }
else {
let err = new Error("Unauthorized");
err.status = 401;
next(err);
}
};
// Login/-out
app.route('/login')
.get( (req,res)=>{
// Already logged in
if (req.isAuthenticated()) { loginCallback(req,res); }
// Show login page
else { res.render('login'); }
})
.post( passport.authenticate('local',loginOutcome), loginCallback );
app.get('/logout', (req,res)=>{
req.logout();
req.flash('success',`You have been logged out.`);
res.redirect( req.session.next || '/' );
});
// Signup
app.route('/signup')
.get( (req,res)=>{
res.redirect('/login#signup');
})
.post( (req,res,next)=>{
// Send token and alert user
function sendToken(user){
// Create a password token
user.createPassToken((err,token)=>{
if (err){ mw.throwErr(err,req); }
// Email the instructions to continue
mail.send({
from: mail.from,
to: `<${user.email}>`,
subject: 'Complete your Tracman registration',
text: mail.text(`Welcome to Tracman! \n\nTo complete your registration, follow this link and set your password:\n${env.url}/settings/password/${token}`),
html: mail.html(`<p>Welcome to Tracman! </p><p>To complete your registration, follow this link and set your password:<br><a href="${env.url}/settings/password/${token}">${env.url}/settings/password/${token}</a></p>`)
})
.then(()=>{
req.flash('success', `An email has been sent to <u>${user.email}</u>. Check your inbox to complete your registration. `);
res.redirect('/login');
})
.catch((err)=>{
mw.throwErr(err,req);
res.redirect('/login#signup');
});
});
}
// Validate email
req.checkBody('email', 'Please enter a valid email address.').isEmail();
req.sanitizeBody('email').normalizeEmail({remove_dots:false});
// Check if somebody already has that email
User.findOne({'email':req.body.email})
.then( (user)=>{
// User already exists
if (user && user.auth.password) {
req.flash('warning','A user with that email already exists! If you forgot your password, you can <a href="/login/forgot">reset it here</a>.');
res.redirect('/login#login');
next();
}
// User exists but hasn't created a password yet
else if (user) {
// Send another token (or the same one if it hasn't expired)
sendToken(user);
}
// Create user
else {
user = new User();
user.created = Date.now();
user.email = req.body.email;
user.slug = slug(user.email.substring(0, user.email.indexOf('@')));
// Generate unique slug
const slug = new Promise((resolve,reject) => {
(function checkSlug(s,cb){
User.findOne({slug:s})
.then((existingUser)=>{
// Slug in use: generate a random one and retry
if (existingUser){
crypto.randomBytes(6)
.then( (buf)=>{
s = buf.toString('hex');
checkSlug(s,cb);
})
.catch( (err)=>{
mw.throwErr(err,req);
reject();
});
}
// Unique slug: proceed
else { cb(s); }
})
.catch((err)=>{
mw.throwErr(err,req);
reject();
});
})(user.slug, (newSlug)=>{
user.slug = newSlug;
resolve();
});
});
// Generate sk32
const sk32 = new Promise((resolve,reject) => {
crypto.randomBytes(32)
.then( (buf)=>{
user.sk32 = buf.toString('hex');
resolve();
})
.catch( (err)=>{
mw.throwErr(err,req);
reject();
});
});
// Save user and send the token by email
Promise.all([slug, sk32])
.then( ()=>{ user.save(); })
.then( ()=>{ sendToken(user); })
.catch( (err)=>{
mw.throwErr(err,req);
res.redirect('/login#signup');
});
}
})
.catch( (err)=>{
mw.throwErr(err,req);
res.redirect('/signup');
});
});
// Forgot password
app.route('/login/forgot')
.all( (req,res,next)=>{
if (req.isAuthenticated()){ loginCallback(req,res); }
else { next(); }
} )
.get( (req,res,next)=>{
res.render('forgot');
} )
.post( (req,res,next)=>{
// Validate email
req.checkBody('email', 'Please enter a valid email address.').isEmail();
req.sanitizeBody('email').normalizeEmail({remove_dots:false});
User.findOne({'email':req.body.email})
.then( (user)=>{
// No user with that email
if (!user) {
// Don't let on that no such user exists, to prevent dictionary attacks
req.flash('success', `If an account exists with the email <u>${req.body.email}</u>, an email has been sent there with a password reset link. `);
res.redirect('/login');
}
// User with that email does exist
else {
// Create reset token
user.createPassToken( (err,token)=>{
if (err){ next(err); }
// Email reset link
mail.send({
from: mail.from,
to: mail.to(user),
subject: 'Reset your Tracman password',
text: mail.text(`Hi, \n\nDid you request to reset your Tracman password? If so, follow this link to do so:\n${env.url}/settings/password/${token}\n\nIf you didn't initiate this request, just ignore this email. `),
html: mail.html(`<p>Hi, </p><p>Did you request to reset your Tracman password? If so, follow this link to do so:<br><a href="${env.url}/settings/password/${token}">${env.url}/settings/password/${token}</a></p><p>If you didn't initiate this request, just ignore this email. </p>`)
}).then(()=>{
req.flash('success', `If an account exists with the email <u>${req.body.email}</u>, an email has been sent there with a password reset link. `);
res.redirect('/login');
}).catch((err)=>{
mw.throwErr(err,req);
res.redirect('/login');
});
});
}
}).catch( (err)=>{
mw.throwErr(err,req);
res.redirect('/login/forgot');
});
} );
// Android
app.post('/login/app', passport.authenticate('local'), appLoginCallback);
// Token-based (android social)
app.get(['/login/app/google','/auth/google/idtoken'], passport.authenticate('google-token'), appLoginCallback);
// app.get('/login/app/facebook', passport.authenticate('facebook-token'), appLoginCallback);
// app.get('/login/app/twitter', passport.authenticate('twitter-token'), appLoginCallback);
// Social
app.get('/login/:service', (req,res,next)=>{
let service = req.params.service,
sendParams = (service==='google')?{scope:['https://www.googleapis.com/auth/userinfo.profile']}:null;
// Social login
if (!req.user) {
//console.log(`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...`);
passport.authorize(service, sendParams)(req,res,next);
}
// Disconnect social account
else {
//console.log(`Attempting to disconnect ${service} account...`);
req.user.auth[service] = undefined;
req.user.save()
.then(()=>{
req.flash('success', `${mw.capitalize(service)} account disconnected. `);
res.redirect('/settings');
})
.catch((err)=>{
mw.throwErr(err,req);
res.redirect('/settings');
});
}
});
app.get('/login/google/cb', passport.authenticate('google',loginOutcome), loginCallback );
app.get('/login/facebook/cb', passport.authenticate('facebook',loginOutcome), loginCallback );
app.get('/login/twitter/cb', passport.authenticate('twitter',loginOutcome), loginCallback );
};

View File

@ -1,96 +1,101 @@
'use strict';
const slug = require('slug'),
mw = require('../middleware.js'),
User = require('../models/user.js'),
router = require('express').Router();
const mw = require('../middleware.js'),
router = require('express').Router(),
slug = require('slug'),
xss = require('xss'),
User = require('../models.js').user;
// Shortcut to favicon.ico
router.get('/favicon.ico', function(req,res){
res.redirect('/static/img/icon/by/16-32-48.ico');
});
// Index route
router.route('/')
.get(function(req,res,next){
module.exports = router
// Logged in
if ( req.session.passport && req.session.passport.user ){
// Get user
User.findById(req.session.passport.user, function(err, user){
if (err){ mw.throwErr(req,err); }
if (!user){ console.log('Already logged in user not found:', req.session.passport); next(); }
// If user found:
else {
// Open index
res.render('index.html', {
user: user,
error: req.flash('error')[0],
success: req.flash('succcess')[0]
});
}
});
}
// Not logged in
else {
res.render('index.html', {
error: req.flash('error')[0],
success: req.flash('success')[0]
});
}
});
// Settings
router.route('/settings')
// Get settings form
.get(mw.ensureAuth, 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', {user:user});
});
// Set new settings
}).post(mw.ensureAuth, function(req,res,next){
User.findByIdAndUpdate(req.session.passport.user, {$set:{
name: req.body.name,
slug: slug(req.body.slug),
email: req.body.email,
settings: {
units: req.body.units,
defaultMap: req.body.map,
defaultZoom: req.body.zoom,
showSpeed: (req.body.showSpeed)?true:false,
showAlt: (req.body.showAlt)?true:false,
showStreetview: (req.body.showStreet)?true:false
}
}}, function(err, user){
if (err) { console.log('Error updating user settings:',err); mw.throwErr(req,err); }
else { req.flash('success', 'Settings updated. '); }
res.redirect('/map#');
});
// Index
.get('/', (req,res,next)=>{
res.render('index');
})
// Delete user account
.delete(mw.ensureAuth, function(req,res,next){
User.findByIdAndRemove( req.session.passport.user,
function(err) {
if (err) {
console.log('Error deleting user:',err);
mw.throwErr(req,err);
} else {
req.flash('success', 'Your account has been deleted. ');
res.redirect('/');
}
}
);
});
// Help
.get('/help', (req,res)=>{
res.render('help');
})
router.route('/help')
.get(mw.ensureAuth, function(req,res){
res.render('help.html', {user:req.session.passport.user});
});
// Terms of Service and Privacy Policy
.get('/terms', (req,res)=>{
res.render('terms');
})
.get('/privacy', (req,res)=>{
res.render('privacy');
})
// robots.txt
.get('/robots.txt', (req,res)=>{
res.type('text/plain');
res.send("User-agent: *\n"+
"Disallow: /map/*\n"
);
})
// favicon.ico
.get('/favicon.ico', (req,res)=>{
res.redirect('/static/img/icon/by/16-32-48.ico');
})
// Endpoint to validate forms
.get('/validate', (req,res,next)=>{
// Validate unique slug
if (req.query.slug) {
User.findOne({ slug: slug(req.query.slug) })
.then( (existingUser)=>{
if (existingUser && existingUser.id!==req.user.id) {
res.sendStatus(400);
}
else { res.sendStatus(200); }
})
.catch( (err)=>{
console.error(err);
res.sendStatus(500);
});
}
// Validate unique email
else if (req.query.email) {
User.findOne({ email: req.query.email })
.then( (existingUser)=>{
if (existingUser && existingUser.id!==req.user.id) {
res.sendStatus(400);
}
else { res.sendStatus(200); }
})
.catch( (err)=>{
console.error(err);
res.sendStatus(500);
});
}
// Create slug
else if (req.query.slugify) {
res.send(slug(xss(req.query.slugify)));
}
// Sanitize for XSS
else if (req.query.xss) {
res.send(xss(req.query.xss));
}
// 404
else { next(); }
})
// Link to androidapp in play store
.get('/android', (req,res)=>{
res.redirect('https://play.google.com/store/apps/details?id=us.keithirwin.tracman');
})
// Link to iphone app in the apple store
// ... maybe someday
.get('/ios', (req,res)=>{
res.redirect('/help#why-is-there-no-ios-app');
})
module.exports = router;
;

View File

@ -2,58 +2,33 @@
const router = require('express').Router(),
mw = require('../middleware.js'),
secrets = require('../secrets.js'),
User = require('../models/user.js');
env = require('../env/env.js'),
User = require('../models.js').user;
// Redirect to real slug
router.get('/', mw.ensureAuth, (req,res)=>{
res.redirect(`/map/${req.user.slug}`);
});
// Show map
router.get('/:slug?', function(req,res,next){
var mapuser='', user='', cbc=0;
router.get('/:slug?', (req,res,next)=>{
// Confirm sucessful queries
function checkQuery(err,found) {
if (err){ mw.throwErr(req,err); }
if (found){ return found; }
}
// Call renderMap() on completion
function checkCBC() {
cbc+=1;
if (cbc>1){ renderMap(); }
}
// QUERIES
// Get logged in user -> user
if (req.isAuthenticated()) {
User.findById(req.session.passport.user, function(err, found) {
user = checkQuery(err,found);
checkCBC();
});
} else { checkCBC(); }
// Get tracked user -> mapuser
if (req.params.slug) {
User.findOne({slug:req.params.slug}, function(err, found) {
mapuser = checkQuery(err,found);
checkCBC();
});
} else { checkCBC(); }
// Show map
function renderMap() {
// GET /map shows logged-in user's map
if (!mapuser && !user) {
res.redirect('/');
} else {
if (user && !mapuser) { mapuser = user; }
res.render('map.html', {
User.findOne({slug:req.params.slug})
.then( (mapuser)=>{
if (!mapuser){ next(); } //404
else {
res.render('map', {
mapuser: mapuser,
mapApi: secrets.mapAPI,
user: user,
mapApi: env.googleMapsAPI,
user: req.user,
noFooter: '1',
noHeader: (req.query.noheader)?req.query.noheader.match(/\d/)[0]:'',
disp: (req.query.disp)?req.query.disp.match(/\d/)[0]:'' // 0=map, 1=streetview, 2=both
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
});
}
}
}).catch( (err)=>{
mw.throwErr(err,req);
});
});

View File

@ -1,53 +0,0 @@
'use strict';
const router = require('express').Router(),
mw = require('../middleware.js'),
slug = require('slug'),
User = require('../models/user.js');
router.get('/robots.txt', function(req,res){
res.type('text/plain');
res.send("User-agent: *\n"+
"Disallow: /map\n"
);
});
router.get('/validate', function(req,res){
if (req.query.slug) { // validate unique slug
User.findOne({slug:slug(req.query.slug)}, function(err, existingUser){
if (err) { console.log('/validate error:',err); }
if (existingUser && existingUser.id!==req.session.passport.user) { res.sendStatus(400); }
else { res.sendStatus(200); }
});
}
});
router.get('/android', function(req,res){
res.redirect('https://play.google.com/store/apps/details?id=us.keithirwin.tracman');
});
router.get('/license', function(req,res){
res.render('license.html', {user:req.user});
});
router.route('/pro')
.all(mw.ensureAuth, function(req,res,next){
next();
}).get(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', {user:user}); }
});
}).post(function(req,res){
User.findByIdAndUpdate(req.session.passport.user,
{$set:{ isPro:true }},
function(err, user){
if (err){ mw.throwErr(req,err); }
else { req.flash('success','You have been signed up for pro. '); }
res.redirect('/map');
}
);
});
module.exports = router;

359
config/routes/settings.js Normal file
View File

@ -0,0 +1,359 @@
'use strict';
const slug = require('slug'),
xss = require('xss'),
mellt = require('mellt'),
moment = require('moment'),
mw = require('../middleware.js'),
User = require('../models.js').user,
mail = require('../mail.js'),
env = require('../env/env.js'),
router = require('express').Router();
// Validate email addresses
function validateEmail(email) {
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
// Settings form
router.route('/')
.all( mw.ensureAuth, (req,res,next)=>{
next();
} )
// Get settings form
.get( (req,res)=>{
res.render('settings');
} )
// Set new settings
.post( (req,res,next)=>{
// Validate email
const checkEmail = new Promise( (resolve,reject)=>{
// Check validity
if (!validateEmail(req.body.email)) {
req.flash('warning', `<u>${req.body.email}</u> is not a valid email address. `);
resolve();
}
// Check if unchanged
else if (req.user.email===req.body.email) {
resolve();
}
// Check uniqueness
else {
User.findOne({ email: req.body.email })
.then( (existingUser)=>{
// Not unique!
if (existingUser && existingUser.id!==req.user.id) {
//console.log("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");
req.user.newEmail = req.body.email;
// Create token
//console.log(`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}...`);
mail.send({
to: `"${req.user.name}" <${req.body.email}>`,
from: mail.from,
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>`)
})
.then( ()=>{
req.flash('warning',`An email has been sent to <u>${req.body.email}</u>. Check your inbox to confirm your new email address. `);
resolve();
})
.catch(reject);
});
}
})
.catch(reject);
}
});
// Validate slug
const checkSlug = new Promise( (resolve,reject)=>{
// Check existence
if (req.body.slug==='') {
req.flash('warning', `You must supply a slug. `);
resolve();
}
// Check if unchanged
else if (req.user.slug===slug(xss(req.body.slug))) {
resolve();
}
// Check uniqueness
else {
User.findOne({ slug: req.body.slug })
.then( (existingUser)=>{
// Not unique!
if (existingUser && existingUser.id!==req.user.id) {
req.flash('warning', `That slug, <u>${req.body.slug}</u>, is already in use by another user! `);
}
// It's unique
else {
req.user.slug = slug(xss(req.body.slug));
}
})
.then(resolve)
.catch(reject);
}
});
// Set settings when done
Promise.all([checkEmail, checkSlug])
.then( ()=>{
//console.log('Setting settings... ');
// Set values
req.user.name = xss(req.body.name);
req.user.settings = {
units: req.body.units,
defaultMap: req.body.map,
defaultZoom: req.body.zoom,
showScale: (req.body.showScale)?true:false,
showSpeed: (req.body.showSpeed)?true:false,
showAlt: (req.body.showAlt)?true:false,
showStreetview: (req.body.showStreet)?true:false
};
// Save user and send response
//console.log(`Saving new settings for user ${req.user.name}...`);
req.user.save()
.then( ()=>{
//console.log(`DONE! Redirecting user...`);
req.flash('success', 'Settings updated. ');
res.redirect('/settings');
})
.catch( (err)=>{
mw.throwErr(err,req);
res.redirect('/settings');
});
})
.catch( (err)=>{
mw.throwErr(err,req);
res.redirect('/settings');
});
} )
// Delete user account
.delete( (req,res,next)=>{
User.findByIdAndRemove(req.user)
.then( ()=>{
req.flash('success', 'Your account has been deleted. ');
res.redirect('/');
})
.catch( (err)=>{
mw.throwErr(err,req);
res.redirect('/settings');
});
} );
// Confirm email address
router.get('/email/:token', mw.ensureAuth, (req,res,next)=>{
// Check token
if ( req.user.emailToken===req.params.token) {
// Set new email
req.user.email = req.user.newEmail;
req.user.save()
.then( ()=>{
// Delete token and newEmail
req.user.emailToken = undefined;
req.user.newEmail = undefined;
req.user.save();
})
.then( ()=>{
// Report success
req.flash('success',`Your email has been set to <u>${req.user.email}</u>. `);
res.redirect('/settings');
})
.catch( (err)=>{
mw.throwErr(err,req);
res.redirect(req.session.next||'/settings');
});
}
// Invalid token
else {
req.flash('danger', 'Email confirmation token is invalid. ');
res.redirect('/settings');
}
} );
// Set password
router.route('/password')
.all( mw.ensureAuth, (req,res,next)=>{
next();
} )
// Email user a token, proceed at /password/:token
.get( (req,res,next)=>{
// Create token for password change
req.user.createPassToken( (err,token,expires)=>{
if (err){
mw.throwErr(err,req);
res.redirect((req.user)?'/settings':'/login');
}
// Figure out expiration time
let expirationTimeString = (req.query.tz)?
moment(expires).utcOffset(req.query.tz).toDate().toLocaleTimeString(req.acceptsLanguages[0]):
moment(expires).toDate().toLocaleTimeString(req.acceptsLanguages[0])+" UTC";
// Confirm password change request by email.
mail.send({
to: mail.to(req.user),
from: mail.from,
subject: 'Request to change your Tracman password',
text: mail.text(`A request has been made to change your tracman password. If you did not initiate this request, please contact support at keith@tracman.org. \n\nTo change your password, follow this link:\n${env.url}/settings/password/${token}. \n\nThis request will expire 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>`)
})
.then( ()=>{
// Alert user to check email.
req.flash('success',`An link has been sent to <u>${req.user.email}</u>. Click on the link to complete your password change. This link will expire in one hour (${expirationTimeString}). `);
res.redirect((req.user)?'/settings':'/login');
})
.catch( (err)=>{
mw.throwErr(err,req);
res.redirect((req.user)?'/settings':'/login');
});
});
} );
router.route('/password/:token')
// Check token
.all( (req,res,next)=>{
User
.findOne({'auth.passToken': req.params.token})
.where('auth.passTokenExpires').gt(Date.now())
.then((user) => {
if (!user) {
req.flash('danger', 'Password reset token is invalid or has expired. ');
res.redirect( (req.isAuthenticated)?'/settings':'/login' );
} else {
res.locals.passwordUser = user;
next();
}
})
.catch((err)=>{
mw.throwErr(err,req);
res.redirect('/password');
});
} )
// Show password change form
.get( (req,res)=>{
res.render('password');
} )
// Set new password
.post( (req,res,next)=>{
// Validate password
let daysToCrack = mellt.CheckPassword(req.body.password);
if (daysToCrack<10) {
mw.throwErr(new Error(`That password could be cracked in ${daysToCrack} days! Come up with a more complex password that would take at least 10 days to crack. `));
res.redirect(`/settings/password/${req.params.token}`);
}
else {
// Delete token
res.locals.passwordUser.auth.passToken = undefined;
res.locals.passwordUser.auth.passTokenExpires = undefined;
// Create hash
res.locals.passwordUser.generateHash( req.body.password, (err,hash)=>{
if (err){
mw.throwErr(err,req);
res.redirect(`/password/${req.params.token}`);
}
else {
// Save new password to db
res.locals.passwordUser.auth.password = hash;
res.locals.passwordUser.save()
.then( ()=>{
req.flash('success', 'Password set. You can use it to log in now. ');
res.redirect('/login#login');
})
.catch( (err)=>{
mw.throwErr(err,req);
res.redirect('/login#signup');
});
}
} );
}
} );
// Tracman pro
router.route('/pro')
.all( mw.ensureAuth, (req,res,next)=>{
next();
} )
// Get info about pro
.get( (req,res,next)=>{
res.render('pro');
} )
// Join Tracman pro
.post( (req,res)=>{
User.findByIdAndUpdate(req.user.id,
{$set:{ isPro:true }})
.then( (user)=>{
req.flash('success','You have been signed up for pro. ');
res.redirect(req.session.next||'/settings');
})
.catch( (err)=>{
mw.throwErr(err,req);
res.redirect('/pro');
});
} );
module.exports = router;

52
config/routes/test.js Normal file
View File

@ -0,0 +1,52 @@
'use strict';
const router = require('express').Router(),
mellt = require('mellt'),
mw = require('../middleware.js'),
mail = require('../mail.js');
router
.get('/mail', (req,res,next)=>{
mail.send({
to: `"Keith Irwin" <hypergeek14@gmail.com>`,
from: mail.from,
subject: 'Test email',
text: mail.text("Looks like everything's working! "),
html: mail.html("<p>Looks like everything's working! </p>")
})
.then(()=>{
console.log("Test email should have sent...");
res.sendStatus(200);
})
.catch((err)=>{
mw.throwErr(err,req);
res.sendStatus(500);
});
})
.get('/password', (req,res)=>{
res.render('password');
})
.post('/password', (req,res,next)=>{
let daysToCrack = mellt.CheckPassword(req.body.password);
if (daysToCrack<10) {
let err = new Error(`That password could be cracked in ${daysToCrack} days! Come up with a more complex password that would take at least 10 days to crack. `);
mw.throwErr(err,req);
next(err);
}
else {
res.sendStatus(200);
}
})
.get('/settings', (req,res)=>{
res.render('settings');
})
.post('/settings', (req,res)=>{
//TODO: Test validation here?
});
module.exports = router;

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) {
@ -9,10 +9,14 @@ function checkForUsers(io, user) {
// Checks if any sockets are getting updates for this user
//TODO: Use Object.values() after upgrading to node v7
if (Object.keys(io.sockets.connected).map( function(id){
return io.sockets.connected[id];
}).some( function(socket){
return socket.gets==user;
/* if (Object.values(io.sockets.connected).some( (socket)=>{
* return socket.gets==user;
* })) {
*/
if (Object.keys(io.sockets.connected).map( (key)=>{
return io.sockets.connected[key];
}).some( (socket)=>{
return socket.gets===user;
})) {
//console.log(`Activating updates for ${user}.`);
io.to(user).emit('activate','true');
@ -26,77 +30,85 @@ module.exports = {
checkForUsers: checkForUsers,
init: function(io){
io.on('connection', function(socket) {
init: (io)=>{
io.on('connection', (socket)=>{
//console.log(`${socket.id} connected.`);
// Log
//socket.on('log', function(text){
// 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}`);
//});
// This socket can set location (app)
socket.on('can-set', function(userId){
socket.on('can-set', (userId)=>{
//console.log(`${socket.id} can set updates for ${userId}.`);
socket.join(userId, function(){
socket.join(userId, ()=>{
//console.log(`${socket.id} joined ${userId}`);
});
checkForUsers( io, userId );
});
// This socket can receive location (map)
socket.on('can-get', function(userId){
socket.on('can-get', (userId)=>{
socket.gets = userId;
//console.log(`${socket.id} can get updates for ${userId}.`);
socket.join(userId, function(){
socket.join(userId, ()=>{
//console.log(`${socket.id} joined ${userId}`);
socket.to(userId).emit('activate', 'true');
});
});
// Set location
socket.on('set', function(loc){
socket.on('set', (loc)=>{
//console.log(`${socket.id} set location for ${loc.usr}`);
loc.time = Date.now();
// Check for sk32 token
if (!loc.tok) { console.log('!loc.tok for loc:',loc) }
// Check for user and sk32 token
if (!loc.usr){
console.error("❌", new Error(`Recieved an update from ${socket.ip} without a usr!`).message);
}
else if (!loc.tok){
console.error("❌", new Error(`Recieved an update from ${socket.ip} for usr ${loc.usr} without an sk32!`).message);
}
else {
// Get loc.usr
User.findById(loc.usr, function(err, user) {
if (err) { console.log('Error finding user:',err); }
if (!user) { console.log('User not found for loc:',loc); }
User.findById(loc.usr)
.where('sk32').equals(loc.tok)
.then( (user)=>{
if (!user){
console.error("❌", new Error(`Recieved an update from ${socket.ip} for ${loc.usr} with tok of ${loc.tok}, but no such user was found in the db!`).message);
}
else {
// Confirm sk32 token
if (loc.tok!=user.sk32) { console.log('loc.tok!=user.sk32 || ',loc.tok,'!=',user.sk32); }
else {
// Broadcast location
io.to(loc.usr).emit('get', loc);
//console.log(`Broadcasting ${loc.lat}, ${loc.lon} to ${loc.usr}`);
// Save in db as last seen
user.last = {
lat: parseFloat(loc.lat),
lon: parseFloat(loc.lon),
dir: parseFloat(loc.dir||0),
spd: parseFloat(loc.spd||0),
time: loc.time
};
user.save()
.catch( (err)=>{ console.error("❌", err.stack); });
// Broadcast location
io.to(loc.usr).emit('get', loc);
//console.log(`Broadcasting ${loc.lat}, ${loc.lon} to ${loc.usr}`);
// Save in db as last seen
user.last = {
lat: parseFloat(loc.lat),
lon: parseFloat(loc.lon),
dir: parseFloat(loc.dir||0),
spd: parseFloat(loc.spd||0),
time: loc.time
};
user.save(function(err) {
if (err) { console.log('Error saving user last location:'+loc.user+'\n'+err); }
});
}
}
});
})
.catch( (err)=>{ console.error("❌", err.stack); });
}
});
// Shutdown (check for remaining clients)
socket.on('disconnect', function(reason){
socket.on('disconnect', (reason)=>{
//console.log(`${socket.id} disconnected because of a ${reason}.`);
// Check if client was receiving updates
@ -108,9 +120,7 @@ module.exports = {
});
// Log errors
socket.on('error', function(err){
console.log('Socket error! ',err);
});
socket.on('error', (err)=>{ console.error('❌', err.stack); });
});
}

7
nodemon.json Normal file
View File

@ -0,0 +1,7 @@
{
"verbose": true,
"ext": "html, js, json, css",
"events": {
"start": "npm run minify"
}
}

View File

@ -1,25 +1,38 @@
{
"name": "tracman",
"version": "0.5.1",
"version": "0.6.0",
"description": "Tracks user's GPS location",
"main": "server.js",
"dependencies": {
"bcrypt-nodejs": "0.0.3",
"body-parser": "^1.17.1",
"connect-flash": "^0.1.1",
"connect-flash-plus": "^0.2.1",
"cookie-parser": "^1.4.1",
"cookie-session": "^2.0.0-alpha.1",
"express": "^4.15.2",
"express-validator": "^3.1.3",
"firebase": "^3.7.2",
"kerberos": "0.0.17",
"mellt": "^1.0.0",
"moment": "^2.12.0",
"mongodb": "^2.1.4",
"mongoose": "^4.9.0",
"mongoose-unique-validator": "^1.0.5",
"node-jose": "^0.8.0",
"nodemailer": "^3.1.8",
"nunjucks": "^2.3.0",
"passport": "^0.3.2",
"passport-facebook": "^2.1.1",
"passport-facebook-token": "^3.3.0",
"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",
"passport-twitter-token": "^1.3.0",
"slug": "^0.9.1",
"socket.io": "^1.4.4"
"socket.io": "^1.4.4",
"xss": "^0.3.3"
},
"devDependencies": {
"chai": "^3.5.0",
@ -29,6 +42,7 @@
"karma-chrome-launcher": "^1.0.1",
"karma-firefox-launcher": "^1.0.0",
"karma-mocha": "^1.1.1",
"minifier": "^0.8.1",
"mocha": "^2.5.3",
"nodemon": "^1.10.2",
"supertest": "^1.2.0"
@ -36,7 +50,8 @@
"scripts": {
"test": "mocha test.js",
"start": "node server.js",
"dev": "nodemon server.js",
"nodemon": "nodemon --ignore 'static/**/*.min.*' server.js",
"minify": "minify --template .{{filename}}.min.{{ext}} --clean static",
"update": "sudo n stable && sudo npm update --save && sudo npm prune"
},
"repository": {
@ -50,7 +65,7 @@
"map"
],
"author": "Keith Irwin",
"license": "MIT",
"license": "GPL-3.0",
"README": "README.md",
"bugs": {
"url": "https://github.com/Tracman-org/Server/issues"

189
server.js
View File

@ -1,17 +1,18 @@
'use strict';
/* IMPORTS */
const
/* IMPORTS */
const
express = require('express'),
bodyParser = require('body-parser'),
expressValidator = require('express-validator'),
cookieParser = require('cookie-parser'),
cookieSession = require('cookie-session'),
mongoose = require('mongoose'),
nunjucks = require('nunjucks'),
passport = require('passport'),
flash = require('connect-flash'),
secret = require('./config/secrets.js'),
User = require('./config/models/user.js'),
flash = require('connect-flash-plus'),
env = require('./config/env/env.js'),
User = require('./config/models.js').user,
app = express(),
http = require('http').Server(app),
io = require('socket.io')(http),
@ -19,24 +20,37 @@ const
/* SETUP */ {
/* Database */ mongoose.connect(secret.mongoSetup, {
server:{socketOptions:{
keepAlive:1, connectTimeoutMS:30000 }},
replset:{socketOptions:{
keepAlive:1, connectTimeoutMS:30000 }}
});
/* Templates */ nunjucks.configure(__dirname+'/views', {
autoescape: true,
express: app
});
/* Database */ {
// Setup with native ES6 promises
mongoose.Promise = global.Promise;
// Connect to database
mongoose.connect(env.mongoSetup, {
server:{socketOptions:{
keepAlive:1, connectTimeoutMS:30000 }},
replset:{socketOptions:{
keepAlive:1, connectTimeoutMS:30000 }}
})
.then( ()=>{ console.log(`💿 Mongoose connected to mongoDB`); })
.catch( (err)=>{ console.error(`${err.stack}`); });
}
/* Templates */ {
nunjucks.configure(__dirname+'/views', {
autoescape: true,
express: app
});
app.set('view engine','html');
}
/* Session */ {
app.use(cookieParser(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
}));
@ -44,70 +58,104 @@ const
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(expressValidator());
app.use(flash());
}
/* Auth */ {
require('./config/passport.js')(passport);
app.use(passport.initialize());
app.use(passport.session());
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 */ {
app.get('/favicon.ico', function(req,res){
res.redirect('/static/img/icon/by/16-32-48.ico');
});
app.use('/',
require('./config/routes/index.js'),
require('./config/routes/auth.js'),
require('./config/routes/misc.js')
);
app.use(['/map','/trac'], require('./config/routes/map.js'));
app.use('/admin', require('./config/routes/admin.js'));
app.use('/static', express.static(__dirname+'/static'));
// Static files (keep this before setting default locals)
app.use('/static', express.static( __dirname+'/static', {dotfiles:'allow'} ));
// Set default locals available to all views (keep this after static files)
app.get( '*', (req,res,next)=>{
// Path for redirects
let nextPath = ( req.path.substring(0, req.path.indexOf('#')) || req.path );
if ( nextPath.substring(0,6)!=='/login' && nextPath.substring(0,7)!=='/logout' ){
req.session.next = nextPath+'#';
//console.log(`Set redirect path to ${nextPath}#`);
}
// User account
res.locals.user = req.user;
// Flash messages
res.locals.successes = req.flash('success');
res.locals.dangers = req.flash('danger');
res.locals.warnings = req.flash('warning');
next();
} );
// Auth routes
require('./config/routes/auth.js')(app, passport);
// Main routes
app.use( '/', require('./config/routes/index.js') );
// Settings
app.use( '/settings', require('./config/routes/settings.js') );
// Map
app.use( ['/map','/trac'], require('./config/routes/map.js') );
// Site administration
app.use( '/admin', require('./config/routes/admin.js') );
// Testing
if (env.mode == 'development') {
app.use( '/test', require('./config/routes/test.js' ) );
}
}
/* Errors */ {
// Catch-all for 404s
app.use(function(req,res,next) {
app.use( (req,res,next)=>{
if (!res.headersSent) {
var err = new Error('404: Not found: '+req.url);
var err = new Error(`Not found: ${req.url}`);
err.status = 404;
next(err);
}
});
} );
// Production handlers
if (env.mode!=='development') {
app.use( (err,req,res,next)=>{
if (err.status!==404){ console.error(`${err.stack}`); }
if (res.headersSent) { return next(err); }
res.status(err.status||500);
res.render('error', {
code: err.status||500,
message: (err.status<=499)?err.message:"Server error"
});
} );
}
// Handlers
if (secret.env=='production') {
app.use(function(err,req,res,next) {
// Development handlers
else {
app.use( (err,req,res,next)=>{
if (err.status!==404) {
console.error(`${err.stack}`);
}
if (res.headersSent) { return next(err); }
res.status(err.status||500);
res.render('error.html', {
code: err.status
});
});
}
else /* Development */{
app.use(function(err,req,res,next) {
console.log(err);
if (res.headersSent) { return next(err); }
res.status(err.status||500);
res.render('error.html', {
code: err.status,
res.render('error', {
code: err.status||500,
message: err.message,
error: err
stack: err.stack
});
});
} );
}
}
/* Sockets */ {
@ -117,24 +165,25 @@ const
}
/* RUNTIME */ {
console.log('🖥 Starting Tracman server...');
// Listen
http.listen(secret.port, function(){
console.log(
'==========================================\n'+
'Listening at '+secret.url+
'\n=========================================='
);
http.listen( env.port, ()=>{
console.log(`🌐 Listening in ${env.mode} mode on port ${env.port}... `);
// Check for clients for each user
User.find({}, function(err, users){
if (err) { console.log(`DB error finding all users: ${err.message}`); }
users.forEach( function(user){
User.find({})
.then( (users)=>{
users.forEach( (user)=>{
sockets.checkForUsers( io, user.id );
});
})
.catch( (err)=>{
console.error(`${err.stack}`);
});
});
}
module.exports = app;

View File

@ -1,94 +1,66 @@
/* 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:not(.alert-dismissible) {
text-align: center;
}
.alert a {
color: inherit;
text-decoration: underline;
}
.alert a:hover {
color: inherit;
text-decoration: none;
}
input:focus, textarea:focus {
outline: 0;
::-moz-selection {
background: #999;
}
h1, h2, h3, p {
margin: 0 0 20px 0;
/* Elements */
main {
top: 59px;
position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
overflow-y: auto;
}
.container {
padding-right: 5%;
padding-left: 5%;
width: 100%;
margin: 0 auto;
}
.container:after {
content: "";
display: block;
clear: both;
}
section {
padding: 10vh 0 5vh;
}
h1, h2, h3 {
margin: 0 0 5% 0;
position: relative;
z-index: 6;
}
h1,h2,h3,h4 { font-weight: 600; }
/* Font sizes */
h1, h2, h3, h4 { font-weight: 600; }
h1 {
font-size: 48px;
line-height: 46px; }
@ -98,25 +70,14 @@ h2 {
h3 { font-size: 28px; }
h4 { font-size: 20px; }
.red { color: #fb6e3d; }
a {
color: #fbc93d;
text-decoration: none;
p, main ul {
margin-top: 0;
margin-bottom: 10vh;
}
a:hover {
color: #fbc93d;
text-decoration: underline;
hr {
width: 90%;
margin: 10% auto;
}
.light a {
color:#111;
text-decoration: underline;
}
.light a:hover {
color:#111;
text-decoration: none;
}
img {
max-width: 100%;
}
@ -124,84 +85,102 @@ 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;
}
::-moz-selection {
background: #999;
}
/* End Resets, Clears & Defaults */
.container {
width: 100%;
max-width: 1000px;
margin: 0 auto;
}
.container:after {
content: "";
display: block;
clear: both;
}
section {
padding: 100px 0 50px;
}
.btn {
a {
color: #fbc93d;
text-decoration: none;
}
main a:hover:not(.btn) {
color: #fbc93d;
text-decoration: underline;
}
a.underline {
text-decoration: underline;
}
a.underline:hover:not(.btn) {
text-decoration: none;
}
/* Modifiers */
.hide { display: none !important; }
.red, .red:hover { color: #fb6e3d !important; }
.yellow, .yellow:hover { color: #fbc93d !important; }
.green, .green:hover { color: #8ae137 !important; }
.inline { display: inline; }
.inline-block { display: inline-block; }
.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;
}
.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; }
/* Buttons */
.btn {
font-weight:600;
display: inline-block;
padding: 15px 30px;
transition: 200ms;
background: transparent;
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-radius: .5vw;
} .btn:not(:disabled) {
border: 1px solid #666;
transition: 100ms;
cursor: pointer;
-moz-box-shadow:
inset .11vw .18vw .52vw rgba(255,255,255,.2),
inset -.11vw -.18vw .52vw rgba(0,0,0,.4),
.1vw .1vw .52vw #000;
-webkit-box-shadow:
inset .11vw .18vw .52vw rgba(255,255,255,.2),
inset -.11vw -.18vw .52vw rgba(0,0,0,.4),
.1vw .1vw .36vw #000;
box-shadow:
inset .11vw .18vw .52vw rgba(255,255,255,.2),
inset -.11vw -.18vw .52vw rgba(0,0,0,.4),
.1vw .1vw .36vw #000;
} .btn:disabled {
color: #aaa;
border: 1px solid #444;
} .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:not(:disabled) {
color: #fbc93d;
}
.btn .fa {
margin-left: 10px;

File diff suppressed because it is too large Load Diff

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

138
static/css/form.css Normal file
View File

@ -0,0 +1,138 @@
form {
margin: auto;
max-width: 800px;
}
.form-group {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin: 8% 0;
}
/* Sizing */
form label {
font-size: 1.2em;
margin-right: 3%;
}
/* Input formatting */
form input, form textarea, form select {
color: #eee;
background-color: #202020;
background-color: rgba(255,255,255,0.1);
padding: 1% 1.5%;
border-radius: .3vw;
}
form input:not(:disabled), form textarea:not(:disabled), form .input-addon {
border: 1px solid #666;
-moz-box-shadow: inset .11vw .18vw .25vw rgba(0,0,0,.5);
-webkit-box-shadow: inset .11vw .18vw .25vw rgba(0,0,0,.5);
box-shadow: inset .11vw .18vw .25vw rgba(0,0,0,.5);
}
form input:disabled:not(.input-addon), form textarea:disabled,
form select:disabled {
border: 1px solid #444;
}
form input:not(.input-addon):not(.input-with-addon):not([type="radio"]):not([type="checkbox"]),
form .input-with-addon-group {
min-width: 50%;
}
form input:active:not(.input-addon), form textarea:active,
form select:active,
form input:focus:not(.input-addon), form textarea:focus,
form select:focus {
outline: none;
border: 1px solid #fbc93d;
}
form .input-with-addon-group {
display: flex;
}
form .input-addon, form .input-with-addon {
-moz-box-shadow: inset .11vw .18vw .25vw rgba(0,0,0,.5);
-webkit-box-shadow: inset .11vw .18vw .25vw rgba(0,0,0,.5);
box-shadow: inset .11vw .18vw .25vw rgba(0,0,0,.5);
}
form .input-addon {
text-align: center;
width: auto;
}
form .input-with-addon {
flex-grow: 1;
}
form .input-addon.left {
padding: 1% 0 1% 1.5%;
border-right-color: #202020;
border-right-color: rgba(102,102,102,0);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
form .input-with-addon.left {
padding: 1% 1.5% 1% 0;
border-left-color: #202020;
border-left-color: rgba(102,102,102,0);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
form .input-addon.right {
padding: 1% 1.5% 1% 0;
border-left-color: #202020;
border-left-color: rgba(102,102,102,0);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
form .input-with-addon.right {
padding: 1% 0 1% 1.5%;
border-right-color: #202020;
border-right-color: rgba(102,102,102,0);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
::-webkit-input-placeholder {
color: #666;
}:-moz-placeholder {
color: #666;
opacity: 1;
}::-moz-placeholder {
color: #666;
opacity: 1;
}:-ms-input-placeholder {
color: #666;
}
form select:not(:disabled) {
-moz-box-shadow: inset 0.11vw 0.18vw 0.52vw rgba(255,255,255,.2),
inset -0.11vw -0.18vw 0.52vw rgba(0,0,0,.4),
0.1vw 0.1vw 0.36vw #000;
-webkit-box-shadow: inset 0.11vw 0.18vw 0.52vw rgba(255,255,255,.2),
inset -0.11vw -0.18vw 0.52vw rgba(0,0,0,.4),
0.1vw 0.1vw 0.36vw #000;
box-shadow: inset 0.11vw 0.18vw 0.52vw rgba(255,255,255,.2),
inset -0.11vw -0.18vw 0.52vw rgba(0,0,0,.4),
0.1vw 0.1vw 0.36vw #000;
}
form select > option {
background: #222;
color: inherit;
}
form .radio {
min-width: 150px;
display: flex;
justify-content: space-between;
}
form input[type="checkbox"], form input[type="radio"] {
width: auto;
margin: 8px;
}
form input[type="checkbox"]:active, form input[type="radio"]:active,
form input[type="checkbox"]:focus, form input[type="radio"]:focus {
outline: 1px solid #fbc93d;
}
form .btn {
font-size: 1.5em;
}

View File

@ -1,3 +1,4 @@
/* Main */
header {
background: #222;
padding: 0;
@ -5,9 +6,12 @@ header {
top: 0; left: 0;
width: 100%;
z-index: 200;
} header a:hover, header a:focus {
color: #fbc93d;
}
/* Logo */
header .logo {
float: left;
font-family: 'Open Sans', sans-serif;
padding: 13px 23px;
@ -16,53 +20,43 @@ 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);
}
/* Navigation */
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%;
}
/* Hamburger */
header .hamburger {
display: none;
padding: 5px;
@ -70,18 +64,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 +86,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 +139,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);
@ -161,3 +159,64 @@ header .hamburger--slider.is-active .hamburger-inner {
top: 13px;
}
}
/* Alerts */
.alert {
padding: 15px;
border: 1px solid transparent;
border-radius: 4px;
}
noscript .alert-danger {
z-index: 40;
}
.alert-danger {
z-index: 30;
color: #f2dede;
background-color: #a94442;
}
.alert-warning {
z-index: 20;
color: #fcf8e3;
background-color: #8a6d3b;
}
.alert-success {
z-index: 10;
color: #dff0d8;
background-color: #3c763d;
}
.alert.alert-header {
position: relative;
border-radius: 0;
top: 58px;
width: 100%;
}
.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;
}

View File

@ -15,6 +15,7 @@
/* End Animations */
.btn { border-radius: 50px; }
.container > p { margin-bottom: 5vh; }
.splash {
background: #090909;
@ -165,6 +166,15 @@
.light h2 {
margin-bottom: 40px;
}
.light .btn {
color: #111;
background: rgba(0,0,0,0.1);
}
.light .btn:hover:not(.disabled) {
cursor: pointer;
text-decoration: none;
background: rgba(0,0,0,0.2);
}
.disclaimer {
color: #fb6e3d;

105
static/css/login.css Normal file
View File

@ -0,0 +1,105 @@
/* More padding on the bottom */
.container {
padding-bottom: 10vh;
}
/* Sections */
#login, #signup {
width: 50%;
margin: 0 2%;
}
/* Form overrides */
form .form-group {
margin: 8% 0 0;
}
form input:not(.input-addon):not(.input-with-addon),
form .input-with-addon-group {
width: 96%;
margin: 0 auto 5vh;
}
form .input-with-addon, form .input-addon {
margin: 0;
}
form .input-with-addon-group {
width: 100%;
}
p, input, #social-login {
margin-bottom: 5vh;
}
form input.btn[type="submit"] {
margin: 0 2% 5vh;
}
form #social-login {
justify-content: space-around;
flex-wrap: nowrap;
width: 100%;
}
#show {
padding: 1%;
cursor: pointer;
}
/* Social buttons */
#social-login .btn {
padding: 2%;
text-align: center;
margin: 0 3%;
color: #FFF;
}
#social-login .btn .fa {
position: relative;
}
#social-login .btn .text {
font-size: .6em;
}
#social-login .btn.gp {
background: #ce4d39;
}
#social-login .btn.gp:hover {
background: #fb7a66;
}
#social-login .btn.fb {
background: #305891;
}
#social-login .btn.fb:hover {
background: #5d85be;
}
#social-login .btn.tw {
background: #2ca8d2;
}
#social-login .btn.tw:hover {
background: #59d5ff;
}
/* Small buttons */
@media (max-width:600px), (min-width:800px) and (max-width:1200px) {
#social-login .btn {
padding: 0;
width: 60px;
height: 60px;
}
#social-login .btn .text {
display: none;
}
#social-login .btn .fa {
margin: 18px auto;
}
}
/* Single column */
@media (max-width:800px) {
#login, #signup {
width: 100%;
}
section > .flex {
flex-direction: column;
}
section > .flex > div {
width: 100%;
}
hr {
display: block !important;
}
}

View File

@ -2,11 +2,10 @@ body {
color: #fff;
width: 100%;
height: 100%;
background: #000;}
.wrap {
position: absolute;
bottom: 0px;
width: 100%;
background: #000;
}
main {
overflow: hidden;
}
/* Alerts */
@ -21,20 +20,34 @@ body {
/* Map and streetview */
#map, #pano {position:relative;}
#pano {float:right;}
img#panoImg { width:100%; height:100%; }
#panoImg { width:100%; height:100%; }
#notset {display:none}
/* Tracman logo */
.map-logo {
#map-logo {
margin-left: -75px;
background: #444;
background: rgba(0,0,0,.7);
padding: 0 10px 0 75px;
font-size: 2em;
}
.map-logo a { color: #fbc93d; }
#map-logo a:hover {
text-decoration: none;
}
#map-logo img {
position: relative;
top: 3px;
margin-left: 3px;
}
#map-logo .text {
color: #fbc93d;
position: relative;
top: -3px;
margin-left: 3px;
}
/* Timestamp */
.tim {
#timestamp {
color: #000;
font-size: 12px;
padding-left: 5px;
@ -42,43 +55,112 @@ img#panoImg { width:100%; height:100%; }
background-color: rgba(255,255,255,.7);
}
/*TODO: Make signs smaller on mobile */
/* Speed sign */
.spd {
font-size: 32px;
height: 40px;}
.spd-sign {
/* Signs */
#spd-sign, #alt-sign {
text-align: center;
padding: 2%;
border-radius: 3px;
margin: 3%;
}
#spd-sign {
color: #000;
text-align: center;
padding: 5px;
border: 2px solid #000;
border-radius: 3px;
margin: 10px;
background-color: #FFF;
border: 2px solid #000;
}
/* Altitude sign */
.alt-unit, .spd-unit { font-size:12px; }
.alt-label, .spd-label {
font-size:18px;
height:18px;}
.alt {
font-size: 32px;
height: 40px;}
.alt-sign {
#alt-sign {
color: #FFF;
text-align: center;
padding: 5px;
border: 2px solid #FFF;
border-radius: 3px;
margin: 10px;
background-color: #009800;
border: 2px solid #FFF;
}
@media (max-width:300px) {
#spd, #alt {
height: 20px;
font-size: 18px;
}
#alt-unit, #spd-unit {
font-size: 8px;
}
#alt-label, #spd-label {
font-size: 9px;
height: 9px;
}
}
@media (min-width:300px) and (max-width:350px) {
#spd, #alt {
height: 22px;
font-size: 20px;
}
#alt-unit, #spd-unit {
font-size: 9px;
}
#alt-label, #spd-label {
font-size: 11px;
height: 11px;
}
}
@media (min-width:350px) and (max-width:400px) {
#spd, #alt {
height: 30px;
font-size: 28px;
}
#alt-unit, #spd-unit {
font-size: 10px;
}
#alt-label, #spd-label {
font-size: 14px;
height: 14px;
}
}
@media (min-width:400px) {
#spd, #alt {
height: 40px;
font-size: 32px;
}
#alt-unit, #spd-unit {
font-size: 12px;
}
#alt-label, #spd-label {
font-size: 18px;
height: 18px;
}
}
/* Control buttons */
.btn {
#controls {
width: 100vw;
position: absolute;
bottom: 50px;
display: flex;
justify-content: space-around;
}
#controls .btn {
z-index: 50;
background: #222;
} .btn:hover {
height: 10vh;
padding: 2vh 0;
}
#controls .btn .fa {
margin: 0 2vw;
}
#controls .btn:hover {
background: #333;
}
#controls .btn.set, #controls .btn.clear {
width: 30vw;
}
#controls .btn.track {
width: 35vw;
}
@media (max-width:250px) {
#controls .btn { font-size:.8em; }
}
@media (min-width:250px) and (max-width:350px) {
#controls .btn { font-size:1em; }
}
@media (min-width:350px) and (max-width:450px) {
#controls .btn { font-size:1.15em; }
}
@media (min-width:450px) {
#controls .btn { font-size:1.3em; }
}

70
static/css/settings.css Normal file
View File

@ -0,0 +1,70 @@
/* Social button styles */
#social-connect {
flex-wrap: wrap;
}
#social-connect > .btn {
text-align: center;
display: flex;
align-items: center;
margin-left: 1vw;
margin-right: 1vw;
flex-grow: 1;
flex-basis: 0;
font-size: .9em;
}
#social-connect > .btn:hover {
color: #fff;
}
#social-connect > .btn .fa {
font-size: 1.1em;
margin-left: 0;
margin-right: 5%;
}
/* Connected social button styles */
#social-connect > .btn.gp.connected {
border: 2px solid rgb(206,77,57);
}
#social-connect > .btn.fb.connected {
border: 2px solid rgb(48,88,145);
}
#social-connect > .btn.tw.connected {
border: 2px solid rgb(44,168,210);
}
/* Unconnected social button styles */
#social-connect > .btn.gp:not(.connected) {
background: rgb(206,77,57);
}
#social-connect > .btn.gp:not(.connected):hover {
background: rgb(251,122,102);
}
#social-connect > .btn.fb:not(.connected) {
background: rgb(48,88,145);
}
#social-connect > .btn.fb:not(.connected):hover {
background: rgb(93,133,190);
}
#social-connect > .btn.tw:not(.connected) {
background: rgb(44,168,210);
}
#social-connect > .btn.tw:not(.connected):hover {
background: rgb(89,213,255);
}
/* Submit buttons */
#submit-group {
justify-content: space-around;
}
#submit-group .main {
width: 50%;
}
/* Help */
.help {
display: none;
width: 100%;
margin-top: 2%;
margin-bottom: 0;
text-align: right;
}

10002
static/js/common-passwords.js Normal file

File diff suppressed because it is too large Load Diff

17
static/js/footer.js Normal file
View File

@ -0,0 +1,17 @@
'use strict';
/* global $ */
// Push footer to bottom on pages with little content
function setFooter(){
var windowHeight = $(window).height(),
footerBottom = $("footer").offset().top + $("footer").height();
if (windowHeight > footerBottom){
$("footer").css( "margin-top", windowHeight-footerBottom );
}
}
// Execute on page load
$(function(){ setFooter(); });
// Execute on window resize
$(window).resize(function(){ setFooter(); });

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,20 +0,0 @@
'use strict';
jQuery.extend(jQuery.easing,{
easeInOutExpo: function(x, t, b, c, d){
if (t==0) return b;
if (t==d) return b+c;
if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
}
});
$(document).ready(function(){
$('a[href=#]').click(function(e){
e.preventDefault();
$('nav').removeClass('visible');
$('html,body').stop().animate({scrollTop: $('.'+$(this).data('scrollto')).offset().top-65 }, 700, 'easeInOutExpo', function(){});
});
});

117
static/js/map-controls.js Normal file
View File

@ -0,0 +1,117 @@
'use strict';
/* global navigator $ socket userid token mapuser toggleMaps */
$(function(){
var wpid, newloc;
// Set location
$('#set-loc').click(function(){
if (!userid===mapuser._id){ alert('You are not logged in! '); }
else { if (!navigator.geolocation){ alert('Geolocation not enabled. '); }
else { navigator.geolocation.getCurrentPosition(
// Success callback
function(pos){
var newloc = {
tok: token,
usr: userid,
lat: pos.coords.latitude,
lon: pos.coords.longitude,
spd: (pos.coords.speed||0)
};
socket.emit('set', newloc);
toggleMaps(newloc);
console.log('⚜ Set location:',newloc.lat+", "+newloc.lon);
},
// Error callback
function(err) {
alert("Unable to set location.");
console.error('❌️',err.message);
},
// Options
{ enableHighAccuracy:true }
); } }
});
// Track location
$('#track-loc').click(function(){
if (!userid===mapuser._id) { alert('You are not logged in! '); }
else {
// Start tracking
if (!wpid) {
if (!navigator.geolocation) { alert('Unable to track location. '); }
else {
$('#track-loc').html('<i class="fa fa-crosshairs fa-spin"></i>Stop').prop('title',"Click here to stop tracking your location. ");
wpid = navigator.geolocation.watchPosition(
// Success callback
function(pos) {
newloc = {
tok: token,
usr: userid,
lat: pos.coords.latitude,
lon: pos.coords.longitude,
spd: (pos.coords.speed||0)
}; socket.emit('set',newloc);
toggleMaps(newloc);
console.log('⚜ Set location:',newloc.lat+", "+newloc.lon);
},
// Error callback
function(err){
alert("Unable to track location.");
console.error(err.message);
},
// Options
{ enableHighAccuracy:true }
);
}
}
// Stop tracking
else {
$('#track-loc').html('<i class="fa fa-crosshairs"></i>Track').prop('title',"Click here to track your location. ");
navigator.geolocation.clearWatch(wpid);
wpid = undefined;
}
}
});
// Clear location
$('#clear-loc').click(function(){
if (!userid===mapuser._id) { alert('You are not logged in! '); }
else {
// Stop tracking
if (wpid) {
$('#track-loc').html('<i class="fa fa-crosshairs"></i>&emsp;Track');
navigator.geolocation.clearWatch(wpid);
wpid = undefined;
}
// Clear location
newloc = {
tok: token,
usr: userid,
lat:0, lon:0, spd:0
}; socket.emit('set',newloc);
// Turn off map
toggleMaps(newloc);
console.log('⚜ Cleared location');
}
});
});

313
static/js/map.js Normal file
View File

@ -0,0 +1,313 @@
'use strict';
/* global $ io google mapuser userid disp noHeader */
// Variables
var map, pano, marker, elevator;
const mapElem = document.getElementById('map'),
panoElem = document.getElementById('pano'),
socket = io('//'+window.location.hostname);
function waitForElements(vars,cb){
if ( vars.every(function(v){ return v!==undefined; }) ){
cb();
} else {
setTimeout(waitForElements(vars,cb), 100);
}
}
function onConnect(socket,userid,mapuserid) {
// Can get location
socket.emit('can-get', mapuserid );
console.log("🚹 Receiving updates for",mapuserid);
// Can set location too
if (mapuserid===userid) {
socket.emit('can-set', userid );
console.log("🚹 Sending updates for",userid);
}
}
// socket.io stuff
socket
.on('connect', function(){
console.log("⬆️ Connected!");
waitForElements([mapuser,userid], function() {
onConnect(socket,userid,mapuser._id);
});
})
.on('disconnect', function(){
console.log("⬇️ Disconnected!");
})
.on('error', function (err){
console.error('❌️',err.message);
});
// Parse location
function parseLoc(loc) {
loc.spd = (mapuser.settings.units=='standard')?parseFloat(loc.spd)*2.23694:parseFloat(loc.spd);
loc.dir = parseFloat(loc.dir);
loc.lat = parseFloat(loc.lat);
loc.lon = parseFloat(loc.lon);
loc.time = new Date(loc.time).toLocaleString();
loc.glatlng = new google.maps.LatLng(loc.lat, loc.lon);
return loc;
}
// Show/hide map if location is set/unset
function toggleMaps(loc) {
if (loc.lat===0&&loc.lon===0) {
$('#map').hide();
$('#pano').hide();
$('#notset').show();
} else {
$('#map').show();
$('#pano').show();
$('#notset').hide();
}
}
// Toggle maps on page load
$(function() {
toggleMaps(mapuser.last);
});
// Google maps API callback
window.gmapsCb = function() {
//console.log("gmapsCb() called");
// Make sure everything's ready...
waitForElements([mapuser,disp,noHeader], 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,
scaleControl: mapuser.settings.showScale,
draggable: false,
zoom: mapuser.settings.defaultZoom,
streetViewControl: false,
zoomControlOptions: {position: google.maps.ControlPosition.LEFT_TOP},
mapTypeId: (mapuser.settings.defaultMap=='road')?google.maps.MapTypeId.ROADMAP:google.maps.MapTypeId.HYBRID
});
marker = new google.maps.Marker({
position: { lat:mapuser.last.lat, lng:mapuser.last.lon },
title: mapuser.name,
map: map,
draggable: false
});
map.addListener('zoom_changed',function(){
map.setCenter(marker.getPosition());
});
// Create iFrame logo
if (noHeader!=='0') {
//console.log("Creating iFrame logo...");
const logoDiv = document.createElement('div');
logoDiv.id = 'map-logo';
logoDiv.innerHTML = '<a href="https://tracman.org/">'+
'<img src="https://tracman.org/static/img/style/logo-28.png" alt="[]">'+
"<span class='text'>Tracman</span></a>";
map.controls[google.maps.ControlPosition.BOTTOM_LEFT].push(logoDiv);
}
// Create update time block
//console.log("Creating time block...");
const timeDiv = document.createElement('div');
timeDiv.id = 'timestamp';
if (mapuser.last.time) {
timeDiv.innerHTML = 'location updated '+new Date(mapuser.last.time).toLocaleString();
}
map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(timeDiv);
// Create speed block
if (mapuser.settings.showSpeed) {
//console.log("Creating speed sign...");
const speedSign = document.createElement('div'),
speedLabel = document.createElement('div'),
speedText = document.createElement('div'),
speedUnit = document.createElement('div');
speedLabel.id = 'spd-label';
speedLabel.innerHTML = 'SPEED';
speedText.id = 'spd';
speedText.innerHTML = (mapuser.settings.units=='standard')?(parseFloat(mapuser.last.spd)*2.23694).toFixed():mapuser.last.spd.toFixed();
speedUnit.id = 'spd-unit';
speedUnit.innerHTML = (mapuser.settings.units=='standard')?'m.p.h.':'k.p.h.';
speedSign.id = 'spd-sign';
speedSign.appendChild(speedLabel);
speedSign.appendChild(speedText);
speedSign.appendChild(speedUnit);
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(speedSign);
}
// Create altitude block
if (mapuser.settings.showAlt) {
//console.log("Creating altitude sign...");
const elevator = new google.maps.ElevationService,
altitudeSign = document.createElement('div'),
altitudeLabel = document.createElement('div'),
altitudeText = document.createElement('div'),
altitudeUnit = document.createElement('div');
altitudeLabel.id = 'alt-label';
altitudeText.id = 'alt';
altitudeUnit.id = 'alt-unit';
altitudeSign.id = 'alt-sign';
altitudeText.innerHTML = '';
altitudeLabel.innerHTML = 'ALTITUDE';
getAltitude(new google.maps.LatLng(mapuser.last.lat,mapuser.last.lon), elevator, function(alt) {
if (alt) { altitudeText.innerHTML = (mapuser.settings.units=='standard')?(alt*3.28084).toFixed():alt.toFixed(); }
});
altitudeUnit.innerHTML = (mapuser.settings.units=='standard')?'feet':'meters';
altitudeSign.appendChild(altitudeLabel);
altitudeSign.appendChild(altitudeText);
altitudeSign.appendChild(altitudeUnit);
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(altitudeSign);
}
}
// Create streetview
if (disp!=='0' && mapuser.settings.showStreetview) {
//console.log("Creating streetview...");
updateStreetView(parseLoc(mapuser.last),10);
}
});
};
// Got location
socket.on('get', function(loc) {
console.log("🌐️ Got location:",loc.lat+", "+loc.lon);
// Parse location
loc = parseLoc(loc);
// Update map
if (disp!=='1') {
// Update time
$('#timestamp').text('location updated '+loc.time);
// Show or hide map
toggleMaps(loc);
// Update marker and map center
map.setCenter({ lat:loc.lat, lng:loc.lon });
marker.setPosition({ lat:loc.lat, lng:loc.lon });
// Update speed
if (mapuser.settings.showSpeed) {
$('#spd').text( loc.spd.toFixed() );
}
// Update altitude
if (mapuser.settings.showAlt) {
getAltitude({
lat:loc.lat,
lng:loc.lon
}, elevator, function(alt) {
if (alt) {
$('#alt').text( (mapuser.settings.units=='standard')?(alt*3.28084).toFixed():alt.toFixed() );
}
});
}
}
// Update street view
if (disp!=='0' && mapuser.settings.showStreetview) {
updateStreetView(loc,10);
}
});
// Check altitude
function getAltitude(loc,elev,cb){
//console.log("Getting altitude...");
elev = elev || new google.maps.ElevationService;
elev.getElevationForLocations({
'locations': [loc]
}, function(results, status) {
if (status === google.maps.ElevationStatus.OK && results[0]) {
cb(results[0].elevation);
}
});
}
// Get street view imagery
//TODO: Use global loc object?
function getStreetViewData(loc,rad,cb) {
if (!sv) { var sv=new google.maps.StreetViewService(); }
sv.getPanorama({
location: {
lat: loc.lat,
lng: loc.lon
},
radius:rad
}, function(data,status){ switch (status){
// Success
case google.maps.StreetViewStatus.OK:
cb(data);
break;
// No results in that radius
case google.maps.StreetViewStatus.ZERO_RESULTS:
// Square the radius and try again
getStreetViewData(loc,rad*rad*.5,cb);
break;
// Error
default:
console.error(new Error('❌️ Street view not available: '+status).message);
} });
}
// Update streetview
function updateStreetView(loc) {
//console.log("Updating streetview...");
// Moving
if (loc.spd>1) {
var imgElem = document.getElementById('panoImg');
getStreetViewData(loc, 50, function(data){
if (!imgElem) {
// Create image
pano = undefined;
$('#pano').empty();
$('#pano').append($('<img>',{
alt: 'Street view image',
src: 'https://maps.googleapis.com/maps/api/streetview?size=800x800&location='+loc.lat+','+loc.lon+'&fov=90&heading='+loc.dir+'&key={{api}}',
id: 'panoImg'
}));
}
// Set image
$('#panoImg').attr('src','https://maps.googleapis.com/maps/api/streetview?size='+$('#pano').width()+'x'+$('#pano').height()+'&location='+data.location.latLng.lat()+','+data.location.latLng.lng()+'&fov=90&heading='+loc.dir+'&key={{api}}');
});
}
// Not moving and pano not set
else if (pano==null) {
getStreetViewData(loc, 10, function(data){
// Create panorama
$('#pano').empty();
const panoOptions = {
panControl: false,
zoomControl: false,
addressControl: false,
linksControl: false,
motionTracking: false,
motionTrackingControl: false
};
pano = new google.maps.StreetViewPanorama(panoElem, panoOptions);
// Set panorama
pano.setPano(data.location.pano);
pano.setPov({
pitch: 0,
heading: Math.atan((loc.lon-data.location.latLng.lng())/(loc.lat-data.location.latLng.lat()))*(180/Math.PI)
});
});
}
}

209
static/js/mellt.js Normal file
View File

@ -0,0 +1,209 @@
'use strict';
/**
* Mellt
*
* Tests the strength of a password by calculating how long it would take to
* brute force it.
*
* @version 0.1.0
* @link http://mel.lt/ The homepage for this script.
* @link http://www.hammerofgod.com/passwordcheck.aspx Much of this is based
* on the description of Thor's Godly Privacy password strength checker,
* however the actual code below is all my own.
* @link http://xato.net/passwords/more-top-worst-passwords/ The included
* common passwords list is from Mark Burnett's password collection (which
* is excellent). You can of course use your own password file instead.
*/
var Mellt = function() {
/**
* @var integer HashesPerSecond The number of attempts per second you expect
* an attacker to be able to attempt. Set to 1 billion by default.
*/
this.HashesPerSecond = 1000000000;
/**
* @var string CommonPasswords A variable containing an array of common
* passwords to check against. If you include common-passwords.js in your
* HTML after including Mellt.js, the contents of that file will be used
* if this isn't set.
* Set this to null (and don't include common-passwords.js) to skip
* checking common passwords.
*/
this.CommonPasswords = null;
/**
* @var array $CharacterSets An array of strings, each string containing a
* character set. These should proceed in the order of simplest (0-9) to most
* complex (all characters). More complex = more characters.
*/
this.CharacterSets = [
// We're making some guesses here about human nature (again much of this is
// based on the TGP password strength checker, and Timothy "Thor" Mullen
// deserves the credit for the thinking behind this). Basically we're combining
// what we know about users (SHIFT+numbers are more common than other
// punctuation for example) combined with how an attacker will attack a
// password (most common letters first, expanding outwards).
//
// If you want to support passwords that use non-english characters, and
// your attacker knows this (for example, a Russian site would be expected
// to contain passwords in Russian characters) add your characters to one of
// the sets below, or create new sets and insert them in the right places.
"0123456789",
"abcdefghijklmnopqrstuvwxyz",
"abcdefghijklmnopqrstuvwxyz0123456789",
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-=_+",
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-=_+[]\"{}|;':,./<>?`~"
];
};
Mellt.prototype = {
CommonPasswords: null,
/**
* Tests password strength by simulating how long it would take a cracker to
* brute force your password.
*
* Also optionally tests against a list of common passwords (contained in an
* external file) to weed out things like "password", which from a pure brute
* force perspective would be harder to break if it wasn't so common.
*
* The character sets being used in this checker assume English (ASCII)
* characters (no umlauts for example). If you run a non-english site, and you
* suspect the crackers will realize this, you may want to modify the
* character set to include the characters in your language.
*
* @param string $password The password to test the strength of
* @return integer Returns an integer specifying how many days it would take
* to brute force the password (at 1 billion checks a second) or -1 to
* indicate the password was found in the common passwords file. Obviously if
* they don't have direct access to the hashed passwords this time would be
* longer, and even then most computers (at the time of this writing) won't be
* able to test 1 billion hashes a second, but this function measures worst
* case scenario, so... I would recommend you require at least 30 days to brute
* force a password, obviously more if you're a bank or other secure system.
* @throws Exception If an error is encountered.
*/
CheckPassword: function(password) {
// First check passwords in the common password file if available.
// We do this because "password" takes 129 seconds, but is the first
// thing an attacker will try.
if (!this.CommonPasswords && Mellt.prototype.CommonPasswords) {
this.CommonPasswords = Mellt.prototype.CommonPasswords;
}
if (this.CommonPasswords) {
var text = password.toLowerCase();
for (var t=0; t<this.CommonPasswords.length; t++) {
if (this.CommonPasswords[t]==text) {
// If their password exists in the common file, then it's
// zero time to crack this terrible password.
return -1;
}
}
}
// Figure out which character set the password is using (based on the most
// "complex" character in it).
var base = '';
var baseKey = null;
for (var t=0; t<password.length; t++) {
var char = password[t];
var foundChar = false;
for (var characterSetKey=0; characterSetKey<this.CharacterSets.length; characterSetKey++) {
var characterSet = this.CharacterSets[characterSetKey];
if (baseKey<=characterSetKey && characterSet.indexOf(char)>-1) {
baseKey = characterSetKey;
base = characterSet;
foundChar = true;
break;
}
}
// If the character we were looking for wasn't anywhere in any of the
// character sets, assign the largest (last) character set as default.
if (!foundChar) {
base = this.CharacterSets[this.CharacterSets.length-1];
break;
}
}
// Starting at the first character, figure out it's position in the character set
// and how many attempts will take to get there. For example, say your password
// was an integer (a bank card PIN number for example):
// 0 (or 0000 if you prefer) would be the very first password they attempted by the attacker.
// 9999 would be the last password they attempted (assuming 4 characters).
// Thus a password/PIN of 6529 would take 6529 attempts until the attacker found
// the proper combination. The same logic words for alphanumeric passwords, just
// with a larger number of possibilities for each position in the password. The
// key thing to note is the attacker doesn't need to test the entire range (every
// possible combination of all characters) they just need to get to the point in
// the list of possibilities that is your password. They can (in this example)
// ignore anything between 6530 and 9999. Using this logic, 'aaa' would be a worse
// password than 'zzz', because the attacker would encounter 'aaa' first.
var attempts = 0;
var charactersInBase = base.length;
var charactersInPassword = password.length;
for (var position=0; position<charactersInPassword; position++) {
// We power up to the reverse position in the string. For example, if we're trying
// to hack the 4 character PING code in the example above:
// First number * (number of characters possible in the charset ^ length of password)
// ie: 6 * (10^4) = 6000
// then add that same equation for the second number:
// 5 * (10^3) = 500
// then the third numbers
// 2 * (10^2) = 20
// and add on the last number
// 9
// Totals: 6000 + 500 + 20 + 9 = 6529 attempts before we encounter the correct password.
var powerOf = charactersInPassword - position - 1;
// Character position within the base set. We add one on because strpos is base
// 0, we want base 1.
var charAtPosition = base.indexOf(password[position])+1;
// If we're at the last character, simply add it's position in the character set
// this would be the "9" in the pin code example above.
if (powerOf==0) {
attempts = attempts + charAtPosition;
}
// Otherwise we need to iterate through all the other characters positions to
// get here. For example, to find the 5 in 25 we can't just guess 2 and then 5
// (even though Hollywood seems to insist this is possible), we need to try 0,1,
// 2,3...15,16,17...23,24,25 (got it).
else {
// This means we have to try every combination of values up to this point for
// all previous characters. Which means we need to iterate through the entire
// character set, X times, where X is our position -1. Then we need to multiply
// that by this character's position.
// Multiplier is the (10^4) or (10^3), etc in the pin code example above.
var multiplier = Math.pow(charactersInBase,powerOf);
// New attempts is the number of attempts we're adding for this position.
var newAttempts = charAtPosition * multiplier;
// Add that on to our existing number of attempts.
attempts = attempts + newAttempts;
}
}
// We can (worst case) try a billion passwords a second. Calculate how many days it
// will take us to get to the password.
var perDay = this.HashesPerSecond*60*60*24;
// This allows us to calculate a number of days to crack. We use days because anything
// that can be cracked in less than a day is basically useless, so there's no point in
// having a smaller granularity (hours for example).
var days = attempts / perDay;
// If it's going to take more than a billion days to crack, just return a billion. This
// helps when code outside this function isn't using bcmath. Besides, if the password
// can survive 2.7 million years it's probably ok.
if (days>1000000000) {
return 1000000000;
}
return Math.round(days);
}
};

File diff suppressed because one or more lines are too long

98
static/js/password.js Normal file
View File

@ -0,0 +1,98 @@
'use strict';
/* global $ Mellt */
const mellt = new Mellt();
function checkMatch(){
$('#submit').prop('title',"You need to type your password again before you can save it. ");
// They match
if ( $('#p1').val() === $('#p2').val() ) {
$('#submit').prop('disabled',false).prop('title',"Click here to save your password. ");
}
// User has retyped, but they don't match yet
else if ($('#p2').val()!=='') {
$('#password-help').text("Those passwords don't match... ").css({'color':'#fb6e3d'});
$('#submit').prop('disabled',true).prop('title',"You need to type the same password twice before you can save it. ");
}
}
// On page load
$(function(){
// On typing password
$('.password').keyup(function(){
// Nothing entered
if ( $('#p1').val()==='' && $('#p2').val()==='' ){
$('#password-help').hide();
$('#submit').prop('disabled',true).prop('title',"You need to enter a password first. ");
}
// Only second password entered
else if ($('#p1').val()==='') {
$('#password-help').show().text("Those passwords don't match... ");
$('#submit').prop('disabled',true).prop('title',"You need to type the same password twice correctly before you can save it. ");
}
// At least first password entered
else {
$('#password-help').show();
// Check first password
var daysToCrack = mellt.CheckPassword($('#p1').val());
// Not good enough
if (daysToCrack<0) {
$('#password-help').text("That's is one of the world's most commonly used passwords. You may not use it for Tracman and should not use it anywhere. ").css({'color':'#fb6e3d'});
$('#submit').prop('disabled',true).prop('title',"You need to come up with a better password. ");
}
else if (daysToCrack<1) {
$('#password-help').text("That password is pretty bad. It could be cracked in less than a day. Try adding more words, numbers, or symbols. ").css({'color':'#fb6e3d'});
$('#submit').prop('disabled',true).prop('title',"You need to come up with a better password. ");
}
else if (daysToCrack<10) {
$('#password-help').text("That password isn't good enough. It could be cracked in "+daysToCrack+" day"+(daysToCrack!=1?'s':'')+". Try adding another word, number, or symbol. ").css({'color':'#fb6e3d'});
$('#submit').prop('disabled',true).prop('title',"You need to come up with a better password. ");
}
// Good enough
else if (daysToCrack<=30) {
$('#password-help').text("That password is good enough, but it could still be cracked in "+daysToCrack+" days. ").css({'color':'#eee'});
checkMatch();
}
else if (daysToCrack<=365) {
$('#password-help').text("That password is good. It would take "+daysToCrack+" days to crack. ").css({'color':'#8ae137'});
checkMatch();
}
else if (daysToCrack<1000000000) {
var years = Math.round(daysToCrack / 365 * 10) / 10;
if (years>1000000) {
years = (Math.round(years/1000000*10)/10)+' million';
}
if (years>1000) {
years = (Math.round(years/1000))+' thousand';
}
$('#password-help').text("That password is great! It could take up to "+years+" years to crack!").css({'color':'#8ae137'});
checkMatch();
}
else {
$('#password-help').text("That password is amazing! It is virtually impossible to crack!").css({'color':'#8ae137'});
checkMatch();
}
}
});
// On checking 'show'
$('#show').click(function(){
if ($(this).is(':checked')) {
$('.password').attr('type','text');
} else {
$('.password').attr('type','password');
}
});
});

164
static/js/settings.js Normal file
View File

@ -0,0 +1,164 @@
'use strict';
/* global location $ */
// Validate email addresses
function validateEmail(email) {
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
// Replace inputed value with response
function replaceFromEndpoint(type, selector, cb) {
$.get('/validate?'+type+'='+$(selector).val())
.done(function(data){
$(selector).val(data);
cb();
});
}
// On page load
$(function(){
var slugNotUnique, emailNotUnique;
// Set timezone in password change link
$('#password').attr('href',"/settings/password?tz="+new Date().getTimezoneOffset());
// Delete account
$('#delete').click(function(){
if (confirm("Are you sure you want to delete your account? This CANNOT be undone! ")) {
$.ajax({
url: '/settings',
type: 'DELETE',
success: function(){
location.reload();
},
fail: function(){
alert("Failed to delete account!");
}
});
}
});
function validateForm(input) {
// Perform basic check, then validate uniqueness
basicCheck(function(){ validateUniqueness(input); });
function basicCheck(cb){
var checkedCount = 0;
// Check slug
if (!$('#slug-input').val()){
$('#slug-help').show().text("A slug is required. ");
$('#submit-group .main').prop('disabled',true).prop('title',"You need to enter a slug. ");
if (checkedCount>0) {cb();} else {checkedCount++;}
}
else {
if (!slugNotUnique){ $('#slug-help').hide(); }
if (checkedCount>0) {cb();} else {checkedCount++;}
}
// Check email
if (!$('#email-input').val()){
$('#email-help').show().text("An email is required. ");
$('#submit-group .main').prop('disabled',true).prop('title',"You need to enter an email address. ");
if (checkedCount>0) {cb();} else {checkedCount++;}
}
else if (!validateEmail($('#email-input').val())) {
$('#email-help').show().text("You must enter a valid email address. ");
$('#submit-group .main').prop('disabled',true).prop('title',"You need to enter a valid email address. ");
if (checkedCount>0) {cb();} else {checkedCount++;}
}
else {
if (!emailNotUnique){ $('#email-help').hide(); }
if (checkedCount>0) {cb();} else {checkedCount++;}
}
}
function validateUniqueness(input){
function recheckBasic(){
if ($('#email-help').is(":visible") && $('#email-help').text().substring(0,25)!=="Unable to confirm unique ") {
$('#submit-group .main').prop('disabled',true).prop('title',"You need to supply a different email address. ");
}
else if ($('#slug-help').is(":visible") && $('#slug-help').text().substring(0,25)!=="Unable to confirm unique ") {
$('#submit-group .main').prop('disabled',true).prop('title',"You need to supply a different slug. ");
}
else if ( $('#slug-help').text().substring(0,25)==="Unable to confirm unique " ) {
$('#submit-group .main').prop('title',"Unable to confirm unique slug with the server. This might not work... ");
}
else if ( $('#email-help').text().substring(0,25)==="Unable to confirm unique " ) {
$('#submit-group .main').prop('title',"Unable to confirm unique email with the server. This might not work... ");
}
else {
$('#submit-group .main').prop('disabled',false).prop('title',"Click here to save your changes. ");
}
}
// Should server be queried for unique values?
if (input && $('#'+input+'-input').val()) {
if (input==='email' && !validateEmail($('#email-input').val())) {}
// Query server for unique values
else {
$.ajax({
url: '/validate?'+input+'='+$('#'+input+'-input').val(),
type: 'GET',
statusCode: {
// Is unique
200: function(){
$('#'+input+'-help').hide();
if (input==='slug'){ slugNotUnique=false; }
else if (input==='email'){ emailNotUnique=false; }
recheckBasic();
},
// Isn't unique
400: function(){
if (input==='slug'){ slugNotUnique=true; }
else if (input==='email'){ emailNotUnique=true; }
$('#'+input+'-help').show().text("That "+input+" is already in use by another user. ");
$('#submit-group .main').prop('disabled',true).prop('title',"You need to supply a different "+input+". ");
}
} })
// Server error
.error( function(){
if (input==='slug'){ slugNotUnique=undefined; }
else if (input==='email'){ emailNotUnique=undefined; }
$('#'+input+'-help').show().text("Unable to confirm unique "+input+". This might not work... ");
recheckBasic();
});
} }
// Nothing changed. Recheck basic validations
else { recheckBasic(); }
}
}
// Input change listeners
$('#slug-input').change(function(){
if (!$('#slug-input').val()){
$('#slug-help').show().text("A slug is required. ");
$('#submit-group .main').prop('disabled',true).prop('title',"You need to enter a slug. ");
}
else {
$('#slug-help').hide();
replaceFromEndpoint('slugify','#slug-input',function(){
validateForm('slug');
});
}
});
$('#email-input').change(function(){
validateForm('email');
});
$('#name-input').change(function(){
replaceFromEndpoint('xss','#name-input',validateForm);
});
});

File diff suppressed because one or more lines are too long

208
test.js
View File

@ -1,20 +1,11 @@
var chai = require('chai'),
const chai = require('chai'),
chaiHttp = require('chai-http'),
request = require('supertest'),
server = require('./server'),
should = chai.should(),
expect = chai.expect();
server = require('./server');
chai.use(chaiHttp);
describe('Index', function() {
// I think this restarts the server after each try?
// var server;
// beforeEach(function() {
// server = require('./server');
// });
// afterEach(function() {
// server.close();
// });
describe('Public', function() {
it('Displays homepage', function(done){
request(server).get('/')
@ -22,6 +13,24 @@ describe('Index', function() {
.end(function(err,res){ done(); });
});
it('Displays help page', function(done){
request(server).get('/help')
.expect(200)
.end(function(err,res){ done(); });
});
it('Displays terms of service', function(done){
request(server).get('/terms')
.expect(200)
.end(function(err,res){ done(); });
});
it('Displays privacy policy', function(done){
request(server).get('/privacy')
.expect(200)
.end(function(err,res){ done(); });
});
it('Displays robots.txt', function(done){
request(server).get('/robots.txt')
.expect(200)
@ -37,60 +46,15 @@ describe('Index', function() {
});
describe('Auth', function() {
describe('User', function() {
it('Creates an account', function(done){
request(server).get('/login')
.expect(200)
.end(function(err,res){
//TODO: google authentication
it('Logs out', function(done){
request(server).get('/logout')
.expect(200)
.end(function(err,res){
it('Logs in', function(done){
request(server).get('/logout')
.expect(200)
.end(function(err,res){
cbc=2;
var deletesAccount = function(done){
it('Deletes own account', function(){
//TODO: Delete account via GUI
});
}
it('Shows own map', function(done){
request(server).get('/map')
.expect(200)
//TODO: Expect no js errors
.end(function(err,res){
if (cbc<2){ deletesAccount(); }
else { cbc--; }
done();
});
});
it('Has the correct account info', function(done){
//TODO: Check account info
if (cbc<2){ deletesAccount(); }
else { cbc--; }
done();
});
done();
});
});
done();
});
});
done();
});
request(server).post('/signup',{"email":"test@tracman.org"})
.expect(200)
.end(function(err,res){ done(); });
});
//TODO: it('Has the correct account info', function(done){
// });
//TODO: it('Logs out', function(done){
//TODO: it('Creates a password', function(done){
// });
@ -98,7 +62,85 @@ describe('Auth', function() {
// });
//TODO: it('Logs out', function(done){
// });
//TODO: it('Forgets password', function(done){
// });
//TODO: it('Changes forgotten password', function(done){
// });
//TODO: it('Logs back in', function(done){
// });
//TODO: it('Changes email address', function(done){
// });
//TODO: it('Changes password', function(done){
// });
//TODO: it('Changes settings', function(done){
// });
//TODO: it('Connects a Google account', function(done){
// });
//TODO: it('Connects a Facebook account', function(done){
// });
//TODO: it('Connects a Twitter account', function(done){
// });
//TODO: it('Logs in with Google', function(done){
// });
//TODO: it('Logs in with Facebook', function(done){
// });
//TODO: it('Logs in with Twitter', function(done){
// });
//TODO: it('Disconnects a Google account', function(done){
// });
//TODO: it('Disconnects a Facebook account', function(done){
// });
//TODO: it('Disconnects a Twitter account', function(done){
// });
//TODO: it('Shows own map', function(done){
// request(server).get('/map')
// .expect(200)
// .end(function(err,res){ done(); });
// });
//TODO: it('Sets own location', function(done){
// });
//TODO: it('Tracks own location', function(done){
// });
//TODO: it('Clears own location', function(done){
// });
@ -107,43 +149,3 @@ describe('Auth', function() {
// });
});
describe('Map controls', function() {
//TODO: it('Sets location', function(done){
// });
//TODO: it('Clears location', function(done){
// });
//TODO: it('Starts tracking', function(done){
// });
//TODO: it('Stops tracking', function(done){
// });
});
describe('Map popups', function() {
//TODO: it('Opens Share popup', function(done){
// });
//TODO: it('Closes Share popup', function(done){
// });
//TODO: it('Opens Settings popup', function(done){
// });
//TODO: it('Closes Settings popup', function(done){
// });
});

View File

@ -3,7 +3,6 @@
{% block head %}
{{ super() }}
<!--<link rel="stylesheet" type="text/css" href="/static/js/jquery-ui-tabs/jquery-ui.min.css">-->
<style>
.container { max-width:90%; }
</style>
@ -38,7 +37,7 @@
<a href="https://plus.google.com/{{usr.googleID}}/">Google</a>
{% endif %}
</td>
<td id='{{usr.id}}-edit'><form action="/admin/users" method="POST">
<td id='{{usr.id}}-edit'><form method="POST">
<button type="submit" class='btn btn-block btn-danger' name="delete" value="{{usr.id}}">DELETE</button>
</form></td>
</tr>
@ -50,7 +49,7 @@
</div>
</section>
<script src="/static/js/moment.min.js"></script>
<script type="text/javascript" src="/static/js/.moment.min.js"></script>
<script type="text/javascript">
/* DATE/TIME FORMATS */ {

View File

@ -1,13 +1,12 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | {{ code }} Error{% endblock %}
{% block title %}{{super()}} | {% if code %}{{code}} {% endif %}Error{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
{% if code %}<h2>{{code}}</h2>{% endif %}
{% if message %}<h3>{{message}}</h3>{% endif %}
{% if error %}<p>{{error}}</p>{% endif %}
{% if code %}<img style="width:100%" src="https://http.cat/{{code}}.jpg">{% endif %}
</div>
<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>
{% 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>
{% endblock %}

26
views/forgot.html Normal file
View File

@ -0,0 +1,26 @@
{% extends 'templates/base.html' %}
{% block head %}
{{super()}}
<link rel="stylesheet" type="text/css" href="/static/css/.form.min.css">
{% endblock %}
{% block main %}
<section class='container'>
<h1>Reset password</h1>
<p>Enter your email below to recieve a link to reset your password. </p>
<form method="post" role="form">
<div class='group'>
<label for="email">Email:</label>
<input name="email" type="email">
</div>
<input class='main btn' type="submit" value="Go">
</form>
</section>
{% endblock %}

View File

@ -4,19 +4,97 @@
{% block main %}
<section class='container'>
<h2>Help</h2>
<p><i>Welcome to Tracman! Here's how to get started. </i></p>
<p><u>Set</u> sets your location once using this device's geolocation. On a GPS-enabled phone, the location will be set to its coordinates. </p>
<p><u>Track</u> sets your location as above, but continues to track your location as long as you keep this window open. On a phone, this can drain the battery fast, so be careful! </p>
<p><u>Clear</u> clears your location instantly. Anyone looking at your map will see a blank screen instead. Use this to hide your location. </p>
<p>Share your location by sending the URL to anyone. They won't need an account to view the map. </p>
<a href="/map" class='btn' style="width:60%; position:relative; left:20%; background:#333">Go to map <i class='fa fa-angle-right'></i></a>
<h1>Help</h1>
<p><i>Welcome to Tracman!</i></p>
<h2 id='website-controls'>Website map controls</h2>
<ul>
<li><u>Set</u> sets your location once using this device's geolocation. On a GPS-enabled phone, the location will be set to its coordinates. </li>
<li><u>Track</u> sets your location as above, but continues to track your location as long as you keep this window open. To track your location in the background, check out the <a href="/android">android app</a>. </li>
<li><u>Clear</u> clears your location instantly. Anyone looking at your map will see a blank screen instead. Use this to hide your location. </li>
</ul>
<h2 id='website-settings'>Website settings</h2>
<ul>
<li><u>Name</u> is your name. It appears at the title of your browsing window and when you hover over the marker on your map. You can put whatever you want, really. </li>
<li><u>Email</u> is your email, used for account recovery and (really) important information, like if Tracman shuts down permanently. Tracman will never send bulk emails. For more information, check out the <a href="/terms">Terms of Service</a>. </li>
<li><u>URL</u> lets you determine the slug your map can be accessed. See <a href="how-do-i-share-my-location">How do I share my location?</a> for more info. </li>
<li><u>Units</u> let you choose metric (meters and kilometers-per-hour) or standard American units (feet and miles-per-hour). </li>
<li><u>Default Map</u> is the default map type that visitors to your map will see. It's either a standard google map or a satellite image. Users will be able to change this. </li>
<li><u>Default Zoom</u> is the default zoom level that visitors to your map will see. It ranges from 1-20, where 1 shows the whole world and 20 shows the most detail. Users will be able to change this. </li>
<li><u>Show speed</u> puts a sign in the top-right corner of the map, which shows your speed. </li>
<li><u>Show altitude</u> puts a sign in the top-right corner of the map, which shows your altitude. See <a href="#how-is-the-altitude-determined">How is the altitude determined?</a> in the FAQ. </li>
<li><u>Show street view</u> shows a Google street view image of your location. See <a href="#what-is-the-streetview-image">What is the street view image?</a> in the FAQ. </li>
</ul>
<h2 id='android'>Android</h2>
<p>The android app is especially buggy, so be careful! </p>
<h3 id='android-settings'>Settings</h3>
<ul>
<li><u>Start service on boot</u> determines whether the Tracman app will start in the background when you turn on your phone. </li>
<li><u>Enable updates</u> turns on and off updates. Your location will be updated in the background, as long as the app is running. A <a href="#android-notification">notification</a> will show whether updates are enabled or not. </li>
<li><u>Update interval</u> determines how often location updates will be sent to the map. See the <a href="https://developers.google.com/android/reference/com/google/android/gms/location/LocationRequest.html#setInterval(long)">location services documentation</a> for more information. </li>
<li><u>Update priority</u> determines how accurate your updates will be, and how much battery the app will use. See the <a href="https://developers.google.com/android/reference/com/google/android/gms/location/LocationRequest#setPriority(int)">location services documentation</a> for more information. </li>
</ul>
<h3 id='android-notification'>The notification</h3>
<p>While tracman is running in the background, the update interval and priority are lessened while nobody's looking at the map. This is to save battery life. The notification will show whether its sending realtime or occasional updates. This is an informal way of seeing if anyone is viewing your location. </p>
<h2 id='faq'>FAQ</h2>
<ul>
<li><a href="#how-do-i-share-my-location">How do I share my location?</a></li>
<li><a href="#how-accurate-is-the-location">How accurate is the location?</a></li>
<li><a href="#how-is-the-altitude-determined">How is the altitude determined?</a></li>
<li><a href="#what-is-the-streetview-image">What is the street view image?</a></li>
<li><a href="#can-i-contribute-to-tracman">Can I contribute to Tracman?</a></li>
<li><a href="#why-is-there-no-ios-app">Why is there no iOS app?</a></li>
</ul>
<h3 id='how-do-i-share-my-location'>How do I share my location?</h3>
<p>You can simply share your map's url with anyone. {% if user %}Your URL is <a href="https://tracman.org/map/{{user.slug}}">https://tracman.org/map/{{user.slug}}</a>{% else %}The URL is <u>https://tracman.org/map/&gt;your-slug&lt;</u>{% endif %}. </p>
<h3 id='how-accurate-is-the-location'>How accurate is the location?</h3>
<p>When using the web app, the location will be as accurate as the underlying geolocation data. It can be pretty accurate if opened on a mobile phone browser, since the device's GPS will be used. On a desktop, the location will be estimated based on your IP address, which can be very inaccurate. Check out the <a href="https://www.w3.org/TR/geolocation-API/">API Specification</a> for way more information. </p>
<p>On android, Tracman uses <a href="https://developers.google.com/android/reference/com/google/android/gms/location/package-summary">Google Play Services location APIs</a> to set your phone's location. This can use your phone's GPS data, or nearby WiFi and cellular towers. Sometimes this can be pretty inaccurate. </p>
<h3 id='how-is-the-altitude-determined'>How is the altitude determined?</h3>
<p>The altitude is not determined using your GPS, because this is notoriously inaccurate. Instead, the <a href="https://developers.google.com/maps/documentation/elevation">Google Maps Elevation API</a> resolves the altitude of the ground at your coordinates. This means that if you are flying, Tracman will show the altitude of the ground beneath you. </p>
<h3 id='what-is-the-streetview-image'>What is the street view image?</h3>
<p>While you are stationary, Tracman will display the google street view panorama closest to your location. Users will be able to pan the image to look around. By default, the image will be oriented towards you. If you are in a building, and there is a street view image of the street outside the building, the panorama will be pointed at the building. </p>
<p>While you are moving, Tracman will display a plain google street view image closest the image cannot be panned. This allows for it to be loaded faster. The image will be oriented to your direction of travel, so if you are driving down a road, it will show the view in the direction you're driving. This can sometimes appear like a slow-frame-rate dashcam video. </p>
<p>The images come from <a href="https://www.google.com/streetview/">Google street view</a>. They are <i>not</i> live images from your location. Mostly, the photos were taken during the daytime when the weather was good. </p>
<h3 id='can-i-contribute-to-tracman'>Can I contribute to Tracman?</h3>
<p>Sure! Tracman has some <a href="https://github.com/Tracman-org/">github repositories</a> you can clone. </p>
<p>I also accept donations to help with development and server fees. You can pay with <a href="https://cash.me/$KeithIrwin">cash</a> or <a href="bitcoin:16KY9k6qdXqDD3mWwr8hrD7ky18AqYSJDo?label=tracman">bitcoin</a>. </p>
<h3 id='why-is-there-no-ios-app'>Why is there no iOS app?</h3>
<p>There are a few reasons I haven't made a version of the <a href="/android">android app</a> for iPhone/iPad/iPod: </p>
<ul>
<li>I would need to learn a new programming language, and spend tons of time developing the app. I can't just copy the android code over. Everything would need to be built from scratch. </li>
<li>Apple charges $100/year in developer fees. </li>
<li>iOS apps can only be built using a mac. </li>
</ul>
</section>
{% endblock %}

View File

@ -1,47 +1,45 @@
{% extends 'templates/base.html' %}
{% block head %}
{{ super() }}
<link href="/static/css/index.css" rel="stylesheet">
{{super()}}
<link href="/static/css/.index.min.css" rel="stylesheet">
{% endblock %}
{% block main %}
<script src="/static/js/index.js"></script>
<section class='splash dark' id='splash'>
<div class='container'>
<h1>Tracman</h1>
<h3>Display your realtime GPS location on a map</h3>
<!--<a class='btn' href="#" data-scrollto="overview">More info<i class='fa fa-angle-down'></i></a>-->
{% if user %}
<a class='btn' href="/map">Map<i class='fa fa-angle-right'></i></a>
<a class='btn' href="/settings">Settings<i class='fa fa-angle-right'></i></a>
{% if user.isAdmin %}
<a class='btn' href="/admin">Admin<i class='fa fa-angle-right'></i></a>
{% endif %}
{% else %}
<a class='btn' href="/map/keith">View example<i class='fa fa-angle-right'></i></a>
<a class='btn' href="#" data-scrollto="join">Join<i class='fa fa-angle-down'></i></a>
<a class='btn' href="/login">Login<i class='fa fa-angle-right'></i></a>
<a class='btn' href="/login#signup">Join<i class='fa fa-angle-right'></i></a>
<a class='btn' href="/login#login">Login<i class='fa fa-angle-right'></i></a>
{% endif %}
</div>
</section>
<section class='overview dark' id='overview'>
<div class='container'>
<div>
<i class='fa fa-mobile'></i>
<h3>Easy-to-use</h3>
<p>Download the android app and log in.&ensp;Then send your friends a link with a map showing your live location.&ensp;</p>
<p>Download the android app and log in. Then send your friends a link with a map showing your live location. </p>
</div>
<div>
<i class='fa fa-bolt'></i>
<h3>Realtime</h3>
<p>Your location updates every second for all the world to see.&ensp;</p>
<p>Your location updates every second for all the world to see. </p>
</div>
<div>
<i class='fa fa-usd'></i>
<h3>Free</h3>
<p>It's free, but you can <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=hypergeek14%40gmail%2ecom&lc=US&item_name=Tracman&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted">donate</a> if you want to help with server expenses.&ensp;</p>
<p>It's free, but you can <a href="https://cash.me/$KeithIrwin">donate</a> if you want to help with server expenses. </p>
</div>
</div>
</section>
@ -51,22 +49,22 @@
<img src="/static/img/style/phone.png" alt="Mobile phone">
<div>
<h2>Setting your location</h2>
<p>You can track your GPS location from your phone's web browsers.&ensp;There's also has an android app which can run in the background.&ensp;With the app, you can: </p>
<p>You can track your GPS location from your phone's web browsers. There's also has an android app which can run in the background. With the app, you can: </p>
<ul>
<li>
<i class='fa fa-toggle-on'></i>
<h4>Turn off tracking</h4>
<p>If you need to go undercover, just turn tracman off with the flip of a switch.&ensp;</p>
<p>If you need to go undercover, just turn tracman off with the flip of a switch. </p>
</li>
<li>
<i class='fa fa-cog'></i>
<h4>Change settings</h4>
<p>Change your settings to show a less accurate location, if you want an air of mystery.&ensp;</p>
<p>Change your settings to show a less accurate location, if you want an air of mystery. </p>
</li>
<li>
<i class='fa fa-battery-3'></i>
<h4>Save energy</h4>
<p>If nobody's tracking you, tracman won't needlessly drain your battery.&ensp;</p>
<p>If nobody's tracking you, tracman won't needlessly drain your battery. </p>
</li>
</ul>
</div>
@ -78,34 +76,33 @@
<img src="/static/img/style/laptop.png" alt="Laptop">
<div>
<h2>The Map</h2>
<p>You'll get a simple webpage with a map to send to friends.&ensp;It'll look <a href="/map/keith">like this</a>.&ensp;</p>
<p>You'll get a simple webpage with a map to send to friends. It'll look <a href="/map/keith">like this</a>. </p>
<ul>
<li>
<i class='fa fa-hand-o-right'></i>
<h4>Easy</h4>
<p>Just send a link to whomever you want.&ensp;Bam, now they know where you are.&ensp;</p>
<p>Just send a link to whomever you want. Bam, now they know where you are. </p>
</li>
<li>
<i class='fa fa-map-marker'></i>
<h4>Precise</h4>
<p>Map updates in realtime with websockets.&ensp;</p>
<p>Map updates in realtime with websockets. </p>
</li>
<li>
<i class='fa fa-cogs'></i>
<h4>Customizable</h4>
<p>Change the map default type and zoom level.&ensp;You can also show speed, altitude, and streetview.&ensp;</p>
<p>Change the map default type and zoom level. You can also show speed, altitude, and streetview. </p>
</li>
</ul>
</div>
</div>
</section>
<section class='disclaimer light' id='disclaimer'>
<section class='disclaimer' id='disclaimer'>
<div class='container'>
<h2>Warning! </h2>
<p>This is beta software, so there are still kinks to be worked out.&ensp;</p>
<p>Also keep in mind that publishing your location online could be a bad idea.&ensp;</p>
<p>I assume no responsibilities.&ensp;</p>
<p>This is beta software, so there are still kinks to be worked out. </p>
<p>Keep in mind that publishing your location online could be a bad idea. </p>
</div>
</section>
@ -113,9 +110,8 @@
<section class='join light' id='join'>
<div class='container'>
<h2>Hook me up!</h2>
<h3>Right now, Tracman is in beta testing.&ensp;Things may break.&ensp;</h3>
<p>You will need a google account to join tracman and log in.&ensp;</p>
<a class='btn btn-lg ' href="/login">Join Tracman</a>
<p>Just click that there button to create an account. </p>
<a class='btn btn-lg' href="/login#signup">Join Tracman</a>
</div>
</section>
{% endif %}

View File

@ -1,31 +0,0 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | License{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
<h2>The MIT License (MIT)</h2>
<h3>Copyright &copy; 2016 Keith Irwin</h3>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: </p>
<p>The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software. </p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. </p>
</div>
</section>
{% endblock %}

93
views/login.html Normal file
View File

@ -0,0 +1,93 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | Login{% 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/.login.min.css">
{% endblock %}
{% block main %}
<section class='container'>
<h1>Welcome!</h1>
<div class='flex'>
<div id='login'>
<h3>Login</h3>
<form method="post">
<div id='social-login' class='flex form-group'>
<a href="/login/google" class='gp btn'>
<i class="fa fa-google-plus"></i>
<span class='text'>Login with Google</span>
</a>
<a href="/login/facebook" class='fb btn'>
<i class="fa fa-facebook"></i>
<span class='text'>Login with Facebook</span>
</a>
<a href="/login/twitter" class='tw btn'>
<i class="fa fa-twitter"></i>
<span class='text'>Login with Twitter</span>
</a>
</div>
<div class='form-group' style="flex-wrap:wrap">
<input type="email" name="email" placeholder="Email" required>
<div id='password-group' class='input-with-addon-group'>
<input id='password' type="password" class='input-with-addon right' placeholder="Password" name="password" minlength="8" maxlength="160" required>
<input id='show' type="text" class='right input-addon yellow' size="4" value="SHOW" readonly>
</div>
</div>
<input type="submit" value="Sign in" class='btn main'>
<p style="margin-bottom:0"><a href="/login/forgot">Forgot your password?</a></p>
</form>
</div>
<hr style="display:none">
<div id='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>
</section>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
/* global $ */
// On page load
$(function(){
// On clocking 'show'
$('#show').click(function(){
if ($('#password').attr('type')==="password") {
$('#password').attr('type','text');
} else {
$('#password').attr('type','password');
}
});
});
</script>
{% endblock %}

View File

@ -1,438 +1,94 @@
{% extends 'templates/base.html' %}
{% block title %}{{super()}} | {{mapuser.name}}{% endblock %}
{% block title %}{{super()}}{% if mapuser.name %} | {{mapuser.name}}{% endif %}{% endblock %}
{% block head %}
{{super()}}
<link href="/static/css/map.css" rel="stylesheet">
<style>
.wrap { top:{% if not noHeader %}58{% else %}0{% endif %}px;}
<link href="/static/css/.map.min.css" rel="stylesheet">
<style>/* These styles include values passed from to the template from the server. Other styles are in map.css */
{% if noHeader!='0' %} main { top:0; } {% endif %}
{% if mapuser.settings.showStreetview and disp!='0' and disp!='1' %}
/* show both */
@media (orientation: landscape) {
#map, #pano {
display:inline-block;
width:50%;
height:99%;
}2
display: inline-block;
width: 50%;
height: 100%;
}
#pano { float:right; }
}
@media (orientation: portrait) {
#map, #pano {
width:100%;
height:50%;
width: 100%;
height: 50%;
}
#pano { bottom:0; }
}
{% elif mapuser.settings.showStreetview and disp=='1' %}
/* show streetview */
#map { display:none; }
#pano {
width:100%;
height:100%;}
#map {display: none}
width: 100%;
height: 100%;
}
{% else %}
/* show map */
#map {
width:100%;
height:100%;}
#pano {display: none}
width: 100%;
height: 100%;
}
#pano { display:none; }
{% endif %}
</style>
{% endblock %}
{% block main %}
<div class='wrap'>
<div id='map'></div>
<div id='pano'></div>
<div id='map'></div>
<div id='pano'></div>
<div id='notset' class='centered alert alert-warning'>
{% if user.id == mapuser.id %}
Your location is unset. You can click 'set' below to set it to your current position.
{% else %}
This user has no location set.
{% endif %}
</div>
<div id='notset' class='centered alert alert-warning'>
{% if user.id == mapuser.id %}
<div id='controls'>
Your location is unset. You can click 'set' below to set it to your current position.
{% else %}
This user has no location set.
{% endif %}
</div>
{% if user.id == mapuser.id %}
<div id='controls'>
{% if mapuser.settings.showStreetview and disp!='0' %}
<style>
#controls {
width: 100vw;
position: absolute;
bottom: 50px;
display: flex;
justify-content: space-around;
} #controls .btn {
padding: 15px 0;
} #controls .btn.set, #controls .btn.clear {
width: 30vw;
} #controls .btn.track {
width: 35vw;
}
{% if mapuser.settings.showStreetview and disp!='0' %}
@media (orientation: portrait) {
#controls { bottom:30px }
}
{% endif %}
</style>
<button class='btn set' onclick="setLocation()">Set</button>
<button class='btn track' onclick="trackLocation()" data-toggle="tooltop" title="Tracking location..." data-trigger="manual"><i class='fa fa-crosshairs'></i>&emsp;Track</button>
<button class='btn clear' onclick="clearLocation()">Clear</button>
</div>
</style>
{% endif %}
</div>
<button id='set-loc' class='btn set' title="Click here to set your location">Set</button>
<button id='track-loc' class='btn track' title="Click here to track your location"><i class='fa fa-crosshairs'></i>Track</button>
<button id='clear-loc' class='btn clear' title="Click here to clear your location">Clear</button>
</div>
{% endif %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.7.3/socket.io.min.js" integrity="sha256-WKvqiY0jZHWQZIohYEmr9KUC5rEaYEOFTq+ByllJK8w=" crossorigin="anonymous"></script>
<script src="https://maps.googleapis.com/maps/api/js?key={{mapApi}}&callback=gmapsCb" async defer></script>
<script>
{% endblock %}
{% block javascript %}
{{super()}}
/* Variables */ {
var wpid, map, pano, sv, marker, elevator,
socket = io('//'+window.location.hostname),
mapuserid = {{mapuser._id |dump|safe}},
userid{% if user._id %} = {{user._id |dump|safe}}{% endif %},
settings = JSON.parse('{{mapuser.settings |dump|safe}}'),
last = JSON.parse('{{mapuser.last |dump|safe}}'),
noHeader = {{noHeader |dump|safe}},
disp = {{disp |dump|safe}}, // 0=map, 1=streetview, 2=both
mapElem = document.getElementById('map'),
panoElem = document.getElementById('pano');
}
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.7.3/socket.io.min.js" integrity="sha256-WKvqiY0jZHWQZIohYEmr9KUC5rEaYEOFTq+ByllJK8w=" crossorigin="anonymous"></script>
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key={{mapApi}}&callback=gmapsCb" async defer></script>
<script type="text/javascript" src="/static/js/.map.min.js"></script>
{% if user.id == mapuser.id %}<script type="text/javascript" src="/static/js/.map-controls.min.js"></script>{% endif %}
<script> /* Variables from server */
function onConnect(socket,userid,mapuserid){
// Can get location
socket.emit('can-get', mapuserid );
//console.log(`Receiving updates for ${mapuserid}.`);
// Can set location too
if (mapuserid==userid){
socket.emit('can-set', userid );
//console.log(`Sending updates for ${userid}.`);
}
}
// Connect to socket.io
socket.on('connect', function(){
//console.log(`Connected... `);
onConnect(socket,userid,mapuserid);
}).on('reconnect', function(){
//console.log(`Reconnected... `);
onConnect(socket,userid,mapuserid);
}).on('error', function (err){
console.error(`Unable to connect because of: ${err.message}`);
});
// Parse location
function parseLoc(loc) {
loc.spd = (settings.units=='standard')?parseFloat(loc.spd)*2.23694:parseFloat(loc.spd)
loc.dir = parseFloat(loc.dir);
loc.lat = parseFloat(loc.lat);
loc.lon = parseFloat(loc.lon);
loc.time = new Date(loc.time).toLocaleString()
loc.glatlng = new google.maps.LatLng(loc.lat, loc.lon);
return loc;
}
// Show/hide map if location is set/unset
function toggleMaps(loc) {
if (loc.lat===0&&loc.lon===0) {
$('#map').hide();
$('#pano').hide();
$('#notset').show();
} else {
$('#map').show();
$('#pano').show();
$('#notset').hide();
}
}
// execute on page load
$(function() {
toggleMaps(last);
});
// Google maps API callback
window.gmapsCb = function() {
// Create map
if (disp!='1'||!settings.showStreetview) {
map = new google.maps.Map( mapElem, {
center: new google.maps.LatLng( last.lat, last.lon ),
panControl: false,
draggable: false,
zoom: settings.defaultZoom,
streetViewControl: false,
zoomControlOptions: {position: google.maps.ControlPosition.LEFT_TOP},
mapTypeId: (settings.defaultMap=='road')?google.maps.MapTypeId.ROADMAP:google.maps.MapTypeId.HYBRID
});
marker = new google.maps.Marker({
position: { lat:last.lat, lng:last.lon },
title: {{ mapuser.name | dump | safe }},
map: map,
draggable: false
});
map.addListener('zoom_changed',function(){
map.setCenter(marker.getPosition());
});
// Create iFrame logo
if (noHeader.length) {
var logoDiv = document.createElement('div');
logoDiv.className = 'map-logo';
logoDiv.innerHTML = '<a href="https://tracman.org/">'+
'<img src="https://tracman.org/static/img/style/logo-28.png" alt="[]">'+
'Tracman</a>';
map.controls[google.maps.ControlPosition.BOTTOM_LEFT].push(logoDiv);
}
// Create update time block
var timeDiv = document.createElement('div');
timeDiv.className = 'tim';
if (last.time) {
timeDiv.innerHTML = 'location updated '+new Date(last.time).toLocaleString();
}
map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(timeDiv);
// Create speed block
if (settings.showSpeed) {
var speedSign = document.createElement('div'),
speedLabel = document.createElement('div'),
speedText = document.createElement('div'),
speedUnit = document.createElement('div');
speedLabel.className = 'spd-label';
speedLabel.innerHTML = 'SPEED';
speedText.className = 'spd';
speedText.innerHTML = (settings.units=='standard')?(parseFloat(last.spd)*2.23694).toFixed():last.spd.toFixed();
speedUnit.className = 'spd-unit';
speedUnit.innerHTML = (settings.units=='standard')?'m.p.h.':'k.p.h.';
speedSign.className = 'spd-sign';
speedSign.appendChild(speedLabel);
speedSign.appendChild(speedText);
speedSign.appendChild(speedUnit);
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(speedSign);
}
// Create altitude block
if (settings.showAlt) {
var elevator = new google.maps.ElevationService;
var altitudeSign = document.createElement('div'),
altitudeLabel = document.createElement('div'),
altitudeText = document.createElement('div'),
altitudeUnit = document.createElement('div');
altitudeLabel.className = 'alt-label';
altitudeText.className = 'alt';
altitudeUnit.className = 'alt-unit';
altitudeSign.className = 'alt-sign';
altitudeText.innerHTML = '';
altitudeLabel.innerHTML = 'ALTITUDE';
getAltitude(new google.maps.LatLng(last.lat,last.lon), elevator, function(alt) {
if (alt) { altitudeText.innerHTML = (settings.units=='standard')?(alt*3.28084).toFixed():alt.toFixed(); }
});
altitudeUnit.innerHTML = (settings.units=='standard')?'feet above sea level':'meters above sea level';
altitudeSign.appendChild(altitudeLabel);
altitudeSign.appendChild(altitudeText);
altitudeSign.appendChild(altitudeUnit);
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(altitudeSign);
}
}
// Create streetview
updateStreetView(parseLoc(last),10);
}
// Get location
socket.on('get', function(loc) {
//console.log(`Received location: ${loc.lat}, ${loc.lon}`);
loc = parseLoc(loc);
if (disp!='1' || !settings.showStreetview) {
$('.tim').text('location updated '+loc.time);
if (settings.showSpeed) { $('.spd').text(loc.spd.toFixed()); }
if (settings.showAlt) {
getAltitude({lat:loc.lat,lng:loc.lon}, elevator, function(alt) {
if (alt) { $('.alt').text((settings.units=='standard')?(alt*3.28084).toFixed():alt.toFixed()); }
});
}
toggleMaps(loc);
map.setCenter({lat:loc.lat,lng:loc.lon});
marker.setPosition({lat:loc.lat,lng:loc.lon});
}
updateStreetView(loc,10);
});
{% if user %}
var token = '{{user.sk32}}';
// Set location
function setLocation() {
if (!userid==mapuserid) {alert('You are not logged in! '); next();}
else {
if (!navigator.geolocation) {alert('Geolocation not enabled. ');}
else {
navigator.geolocation.getCurrentPosition(function(pos){
newloc = {
tok: token,
usr: userid,
lat: pos.coords.latitude,
lon: pos.coords.longitude,
spd: (pos.coords.speed||0)
}
socket.emit('set', newloc);
toggleMaps(newloc);
}, function(err) {
if (err) {
alert("Unable to set location.");
console.error(err.message); }
else {
//console.log('Set position',pos);
}
}, { enableHighAccuracy:true });
}
}
}
// Track location
function trackLocation() {
if (!userid==mapuserid) { alert('You are not logged in! '); }
else {
// Stop tracking
if (wpid) {
$('#controls > .track').html('<i class="fa fa-crosshairs"></i>&emsp;Track').tooltip('hide');
navigator.geolocation.clearWatch(wpid);
wpid = undefined;
// Start tracking
} else {
$('#controls > .track').html('<i class="fa fa-crosshairs fa-spin"></i>&emsp;Stop').tooltip('show');
if (!navigator.geolocation) { alert('Unable to track location. '); }
else {
wpid = navigator.geolocation.watchPosition(function(pos) {
newloc = {
tok: token,
usr: '{{user.id}}',
lat: pos.coords.latitude,
lon: pos.coords.longitude,
spd: (pos.coords.speed||0)
};
socket.emit('set',newloc);
toggleMaps(newloc);
}, function(err){
if (err) {
alert("Unable to track location.");
console.error(err.message);
} else {
// console.log('Tracking position...');
}
}, { enableHighAccuracy:true });
}
}
}
}
// Clear location
function clearLocation() {
if (!userid==mapuserid) { alert('You are not logged in! '); }
else {
// Stop tracking
if (wpid) {
$('#controls > .track').html('<i class="fa fa-crosshairs"></i>&emsp;Track').tooltip('hide');
navigator.geolocation.clearWatch(wpid);
wpid = undefined;
}
newloc = {
tok: token,
usr: userid,
lat:0, lon:0, spd:0
}
socket.emit('set',newloc);
toggleMaps(newloc);
}
}
// Delete account
function deleteAccount() {
if (confirm("Are you sure you want to delete your account? This CANNOT be undone! ")) {
$.ajax({
url: "/map",
type: "DELETE",
success: function(){
location.reload();
}
})
}
}
{% endif %}
// Check altitude
function getAltitude(loc,elev,cb){
elev = elev || new google.maps.ElevationService;
elev.getElevationForLocations({
'locations': [loc]
}, function(results, status) {
if (status === google.maps.ElevationStatus.OK && results[0]) {
cb(results[0].elevation);
}
});
}
// Get street view imagery
function getStreetViewData(loc,rad,cb) {
if (!sv) { var sv=new google.maps.StreetViewService(); }
sv.getPanorama({location:{lat:loc.lat,lng:loc.lon},radius:rad},function(data,status){
if (status===google.maps.StreetViewStatus.ZERO_RESULTS){
getStreetViewData(loc,rad*2,cb);
} else if (status!==google.maps.StreetViewStatus.OK){ console.log('Street view not available: '+status); }
else { cb(data); }
});
}
// Update streetview
function updateStreetView(loc) {
if (loc.spd>1) { // moving
if (settings.showStreetview && disp!='0') {
var imgElem = document.getElementById('panoImg');
getStreetViewData(loc, 50, function(data){
if (!imgElem) {
// Create image
pano = undefined;
$('#pano').empty();
$('#pano').append($('<img>',{
alt: 'Street view image',
src: 'https://maps.googleapis.com/maps/api/streetview?size=800x800&location='+loc.lat+','+loc.lon+'&fov=90&heading='+loc.dir+'&key={{api}}',
id: 'panoImg'
}));
}
// Set image
$('#panoImg').attr('src','https://maps.googleapis.com/maps/api/streetview?size='+$('#pano').width()+'x'+$('#pano').height()+'&location='+data.location.latLng.lat()+','+data.location.latLng.lng()+'&fov=90&heading='+loc.dir+'&key={{api}}');
});
}
} else if (pano==null) { // not moving and pano not set
getStreetViewData(loc, 50, function(data){
// Create panorama
$('#pano').empty();
var panoOptions = {
panControl: false,
zoomControl: false,
addressControl: false,
linksControl: false,
motionTracking: false,
motionTrackingControl: false
};
pano = new google.maps.StreetViewPanorama(panoElem, panoOptions);
// Set panorama
pano.setPano(data.location.pano);
pano.setPov({
pitch: 0,
heading: Math.atan((loc.lon-data.location.latLng.lng())/(loc.lat-data.location.latLng.lat()))*(180/Math.PI)
});
});
}
}
const mapuser = JSON.parse('{{mapuser |dump|safe}}'),
noHeader = "{{noHeader |safe}}",
disp = "{{disp |safe}}", // 0=map, 1=streetview, 2=both
userid = "{{user._id |safe}}",
token = "{{user.sk32 |safe}}";
</script>
{% endblock %}
{% endblock %}

59
views/password.html Normal file
View File

@ -0,0 +1,59 @@
{% extends 'templates/base.html' %}
{% block title %}{{super()}} | Set Password{% endblock %}
{% block head %}
{{super()}}
<link rel="stylesheet" type="text/css" href="/static/css/.form.min.css">
{% endblock %}
{% block main %}
<section class='container'>
<h1>Set Password</h1>
<form id='password-form' role="form" method="post">
<style>
#password-form .password {
flex-grow: 1;
min-width: 0;
margin: 2%;
}
#password-help {
display: none;
}
.form-group > span {
display: flex;
flex-wrap: wrap;
}
</style>
<p>Your password must be at least 8 characters long. You can use any letter, number, symbol, emoji, or spaces. Your password will be stored as a salted hash on the server. </p>
<div class='form-group' style="flex-wrap:wrap">
<span title="Enter your new password here" style="flex-grow:1; max-width:70vw">
<input id='p1' class='form-control password' name="password" type="password" placeholder="enter password" minlength="8" maxlength="160">
<input id='p2' class='form-control password' name="repassword" type="password" placeholder="retype password" minlength="8" maxlength="160">
</span>
<span title="Show your passwords if nobody's looking over your shoulder. " style="margin:auto">
<input id='show' name="show" type="checkbox" style="margin:8px auto">
<label for="show" style="width:100%; text-align:center;">show</label>
</span>
</div>
<p id='password-help' title="Your passwords are checked using Mellt password cracker. "></p>
<div id='submit-group' class='form-group flexbox' style="justify-content:space-around">
<input id='submit' class='btn main' style="min-width:50%" type="submit" value="Save" title="You need to enter a password first. " disabled>
</div>
</form>
</section>
{% endblock %}
{% block javascript %}
{{super()}}
<script type="text/javascript" src="/static/js/.mellt.min.js"></script>
<script type="text/javascript" src="/static/js/.common-passwords.min.js"></script>
<script type="text/javascript" src="/static/js/.password.min.js"></script>
{% endblock %}

22
views/privacy.html Normal file
View File

@ -0,0 +1,22 @@
{% extends 'templates/base.html' %}
{% block title %}{{super()}} | Privacy Policy{% endblock %}
{% block main %}
<section class='container'>
<h2>Privacy Policy</h2>
<p>In lieu of legalease, which I don't speak, here is a quick rundown of what Tracman does with your data (such as location). </p>
<h3 id='location-history'>Location history</h3>
<p>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). </p>
<p>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. </p>
<h3 id='email'>Email addresses</h3>
<p>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. </p>
</section>
{% endblock %}

View File

@ -1,41 +1,43 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | Pro{% endblock %}
{% block title %}{{super()}} | Pro{% endblock %}
{% block head %}{{super()}}
<style>
p { margin-bottom: 18px; }
.main.btn, #already-pro
{ width: 40vw; }
</style>
{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
<h1>Tracman Pro</h1>
<h3>A word from the developer</h3>
<p>Hi Folks, </p>
<p>Glad you're enjoying my website and app. I made the whole thing, from front to backend,
and I'm really proud of it! However, I'm a long-haul trucker by day and coding is just a hobby.
I don't make any money off this website, and I pay the server fees out of my own pocket. Do you
pity me enough to donate some money by <a href="https://www.paypal.com/us/cgi-bin/webscr?cmd=_flow&SESSION=56toCNZRvXj-_J6kC_5D258zMwjyU7hLvxKFQXsIG5_nzVlAhEw2VLFf7K0&dispatch=5885d80a13c0db1f8e263663d3faee8defcd6970d4fd9d661117ac2649af92bb">paypal</a>
or <a href="bitcoin:1GXbuM398Y2tYwSaQhNQHXWF1AN6GeRgQh?label=tracman">bitcoin</a>? </p>
<p>Glad you're enjoying my website and app. I made the whole thing, from front to backend, and I'm really proud of it! However, I'm a long-haul trucker by day and coding is just a hobby. I don't make any money off this website, and I pay the server fees out of my own pocket. Do you pity me enough to donate some <a href="https://cash.me/$KeithIrwin">money</a> or <a href="bitcoin:16KY9k6qdXqDD3mWwr8hrD7ky18AqYSJDo?label=tracman">bitcoin</a>? </p>
<p>To make a little money off this service, I'm going to be offering a pro version with more
features. It'll be cheap, probably $1 or $2 per month. However, while Tracman is in beta,
you can beta test the pro version too. Be sure to inform me about any <a href="/bug">bugs</a>
you encounter or <a href="/suggestion">suggestions</a> you have. And keep in mind that at some
point, when we launch out of beta, Tracman Pro will <em>not</em> be free and <strong>you will
lose your pro membership</strong> unless start paying for it.
<p>To make a little money off this service, I'm going to be offering a pro version with more features. It'll be cheap, probably $1 or $2 per month. While Tracman is in beta, you can test the pro version for free. Be sure to <a href="https://github.com/Tracman-org/Server/issues/new">inform me about any bugs</a> you encounter. And keep in mind that at some point, when we launch out of beta, Tracman Pro will <i>not</i> be free and you will <b>lose</b> your pro membership unless start paying for it. </p>
<p>That said, just click the button below to test out the pro features. Keep in mind, they are
as <a href="/#disclaimer">unstable</a> as the rest of this product.
<p>That said, just click the button below to try pro out. </p>
<p>Cheers, <br>
<a href="https://keithirwin.us/">Keith Irwin</a></p>
<form class='row flexbox' action="#" method="POST">
<form class='flex' action="#" method="POST">
{% if user.isPro %}
<div class='alert alert-success'><i class="fa fa-check-circle"></i> You are already pro! </div>
<div id='already-pro' class='inline-block alert alert-success'>
<i class="fa fa-check-circle"></i>
<span class='text'>You are already pro! </span>
</div>
<a class='btn' href="javascript:history.go(-1)">go back</a>
{% else %}
<button type="submit" class='btn yellow'>GO PRO</button>
<input type="submit" class='btn main' value="Go Pro"></input>
<a class='btn' href="javascript:history.go(-1)">go home</a>
{% endif %}
<a class='btn' href="/dashboard">go home</a>
</form>
</div>

View File

@ -1,134 +1,154 @@
{% extends 'templates/base.html' %}
{% block title %}{{super()}} | Settings{% 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/.settings.min.css">
{% endblock %}
{% block main %}
<section class='container'>
<h1>Settings</h1>
<form id='settings-form' role="form" method="post">
<h2>Settings</h2>
<script src="/static/js/validator.min.js"></script>
<form id='settings-form' class='col-lg-10 col-lg-offset-1 form-horizontal' data-toggle="validator" role="form" method="post">
<div id='name' class='form-group' title="This appears in your page's title. ">
<label class='control-label col-sm-2 col-lg-3' for="name">Name</label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<input class='form-control' name="name" type="text" value="{{user.name}}"
maxlength="160" data-error="Invalid input"><br>
<h2>Account settings</h2>
<div id='name' class='form-group' title="This appears in your page's title. ">
<label for="name">Name</label>
<input id='name-input' class='form-control' name="name" type="text" value="{{user.name}}" maxlength="160">
<p id='name-help' class='red help'></p>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div id='email' class='form-group' title="For account stuff, no dumb newsletters. ">
<label class='control-label col-sm-2 col-lg-3' for="email">Email</label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<input class='form-control' name="email" type="email" value="{{user.email}}"
maxlength="160" data-error="Invalid input"><br>
<div id='email' class='form-group' title="For account stuff, no dumb newsletters. ">
<label for="email">Email</label>
<input id='email-input' class='form-control' name="email" type="email" value="{{user.email}}" maxlength="160">
<p id='email-help' class='red help'></p>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div id='slug' class='form-group' title="This is the URL which shows your location. Be careful whom you share it with! ">
<label class='control-label col-sm-2 col-lg-3' for="slug">URL</label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<span class='input-group-addon'>tracman.org/map/</span>
<input class='form-control' type="text" name="slug" value="{{user.slug}}" required data-remote="/validate"
maxlength="160" data-remote-error="That URL is already taken. " data-error="Invalid input"><br>
<div id='social-connect' class='form-group'>
<a href="/login/google" class='btn gp{% if user.auth.google %} connected{% endif %}'>
<i class="fa fa-google-plus"></i>
{% if user.auth.google %}Disconnect{% else %}Connect{% endif %} Google
</a>
<a href="/login/facebook" class='btn fb{% if user.auth.facebook %} connected{% endif %}'>
<i class="fa fa-facebook"></i>
{% if user.auth.facebook %}Disconnect{% else %}Connect{% endif %} Facebook
</a>
<a href="/login/twitter" class='btn tw{% if user.auth.twitter %} connected{% endif %}'>
<i class="fa fa-twitter"></i>
{% if user.auth.twitter %}Disconnect{% else %}Connect{% endif %} Twitter
</a>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div id='units' class='form-group col-xs-12' title="Select standard units for feet and miles/hour. Select metric units if you are a commie. ">
<label class='control-label col-sm-4 col-lg-3' for="units">Units</label>
<div class='input-group col-sm-8 col-lg-9'>
<div class='radio-inline'><label>
<input type="radio" name="units" value="standard" {% if user.settings.units == 'standard' %}checked{% endif %}>
Standard
</label></div>
<div class='radio-inline'><label>
<input type="radio" name="units" value="metric" {% if user.settings.units == 'metric' %}checked{% endif %}>
Metric
</label></div>
<div id='password-delete' class='form-group'>
<a id='password' class='underline' href="/settings/password" title="Click here to {% if user.auth.password %}change{% else %}set{% endif %} your password. ">{% if user.auth.password %}Change{% else %}Set{% endif %} password</a>
<a id='delete' class='red underline' style="text-align:right" href="#" title="Permently delete your Tracman account. ">Delete account</a>
</div>
</div>
<div id='defaultMap' class='form-group col-xs-12' title="Shows whether to show a satellite image or standard google road map as the default on your page. Visitors will have the option to change this. Note that satellite images load slower. ">
<label class='control-label col-sm-4 col-lg-3' for="map">Default map</label>
<div class='input-group col-sm-8 col-lg-9'>
<div class='radio-inline'><label>
<input type="radio" name="map" value="road" {% if user.settings.defaultMap == 'road' %}checked{% endif %}>
Road
</label></div>
<div class='radio-inline'><label>
<input type="radio" name="map" value="sat" {% if user.settings.defaultMap == 'sat' %}checked{% endif %}>
Satellite
</label></div>
<h2>Map settings</h2>
<div id='slug' class='form-group' title="This is the URL which shows your location. Be careful whom you share it with! ">
<label for="slug">URL</label>
<div class='input-with-addon-group'>
<input type="text" class='left input-addon' size="13" value="tracman.org/map/" disabled readonly>
<input type="text" class='input-with-addon left' id='slug-input' name="slug" value="{{user.slug}}" maxlength="160" required>
</div>
<p id='slug-help' class='red help'></p>
</div>
</div>
<div id='defaultZoom' class='form-group col-xs-12' title="Shows the initial map zoom level on your page. A higher number means more zoom. Note that the size of the viewing window will also have an effect on how much of the map a visitor can see. ">
<label class='control-label col-xs-6 col-sm-4 col-lg-3' for="map">Default zoom</label>
<div class='input-group col-xs-6 col-sm-8 col-lg-9'>
<select class='c-select' name="zoom">
<option {% if user.settings.defaultZoom==1 %}selected {% endif %}value="1">1 World</option>
<option {% if user.settings.defaultZoom==2 %}selected {% endif %}value="2">2</option>
<option {% if user.settings.defaultZoom==3 %}selected {% endif %}value="3">3</option>
<option {% if user.settings.defaultZoom==4 %}selected {% endif %}value="4">4</option>
<option {% if user.settings.defaultZoom==5 %}selected {% endif %}value="5">5 Landmass</option>
<option {% if user.settings.defaultZoom==6 %}selected {% endif %}value="6">6</option>
<option {% if user.settings.defaultZoom==7 %}selected {% endif %}value="7">7</option>
<option {% if user.settings.defaultZoom==8 %}selected {% endif %}value="8">8</option>
<option {% if user.settings.defaultZoom==9 %}selected {% endif %}value="9">9</option>
<option {% if user.settings.defaultZoom==10 %}selected {% endif %}value="10">10 City</option>
<option {% if user.settings.defaultZoom==11 %}selected {% endif %}value="11">11</option>
<option {% if user.settings.defaultZoom==12 %}selected {% endif %}value="12">12</option>
<option {% if user.settings.defaultZoom==13 %}selected {% endif %}value="13">13</option>
<option {% if user.settings.defaultZoom==14 %}selected {% endif %}value="14">14</option>
<option {% if user.settings.defaultZoom==15 %}selected {% endif %}value="15">15 Streets</option>
<option {% if user.settings.defaultZoom==16 %}selected {% endif %}value="16">16</option>
<option {% if user.settings.defaultZoom==17 %}selected {% endif %}value="17">17</option>
<option {% if user.settings.defaultZoom==18 %}selected {% endif %}value="18">18</option>
<option {% if user.settings.defaultZoom==19 %}selected {% endif %}value="19">19</option>
<option {% if user.settings.defaultZoom==20 %}selected {% endif %}value="20">20 Buildings</option>
</select>
<div id='units' class='form-group' title="Select standard units for feet and miles/hour. Select metric units if you are a commie. ">
<label for="units">Units</label>
<div class='radio-group'>
<div class='radio'>
<label>Standard</label>
<input type="radio" name="units" value="standard" {% if user.settings.units == 'standard' %}checked{% endif %}>
</div>
<div class='radio'>
<label>Metric</label>
<input type="radio" name="units" value="metric" {% if user.settings.units == 'metric' %}checked{% endif %}>
</div>
</div>
</div>
</div>
<div id='showSpeed' class='form-group col-xs-12' title="{% if not user.isPro %}PRO ONLY! {% endif %}Shows a spedometer on the map.">
<label class='control-label col-xs-6 col-sm-4 col-lg-3' for="showSpeed">Show speed{% if not user.isPro %} <span class='red'>(PRO)</span>{% endif %}</label>
<div class='input-group col-xs-6 col-sm-8 col-lg-9'>
<input class='form-control' name="showSpeed" type="checkbox" {% if not user.isPro %}disabled {% elif user.settings.showSpeed %}checked{% else %}{% endif %}><br>
<div id='defaultMap' class='form-group' title="Shows whether to show a satellite image or standard google road map as the default on your page. Visitors will have the option to change this. Note that satellite images load slower. ">
<label for="map">Default map</label>
<div class='radio-group'>
<div class='radio'>
<label>Road</label>
<input type="radio" name="map" value="road" {% if user.settings.defaultMap == 'road' %}checked{% endif %}>
</div>
<div class='radio'>
<label>Satellite</label>
<input type="radio" name="map" value="sat" {% if user.settings.defaultMap == 'sat' %}checked{% endif %}>
</div>
</div>
</div>
</div>
<div id='showAltitude' class='form-group col-xs-12' title="{% if not user.isPro %}PRO ONLY! {% endif %}Shows the current elevation on the map. ">
<label class='control-label col-xs-6 col-sm-4 col-lg-3' for="showAlt">Show altitude{% if not user.isPro %} <span class='red'>(PRO)</span>{% endif %}</label>
<div class='input-group col-xs-6 col-sm-8 col-lg-9'>
<input class='form-control' name="showAlt" type="checkbox" {% if not user.isPro %}disabled {% elif user.settings.showAlt %}checked{% else %}{% endif %}><br>
<div id='defaultZoom' class='form-group' title="Shows the initial map zoom level on your page. A higher number means more zoom. Note that the size of the viewing window will also have an effect on how much of the map a visitor can see. ">
<label for="map">Default zoom</label>
<select name="zoom">
<option {% if user.settings.defaultZoom==1 %}selected {% endif %}value="1">1 World</option>
<option {% if user.settings.defaultZoom==2 %}selected {% endif %}value="2">2</option>
<option {% if user.settings.defaultZoom==3 %}selected {% endif %}value="3">3</option>
<option {% if user.settings.defaultZoom==4 %}selected {% endif %}value="4">4</option>
<option {% if user.settings.defaultZoom==5 %}selected {% endif %}value="5">5 Landmass</option>
<option {% if user.settings.defaultZoom==6 %}selected {% endif %}value="6">6</option>
<option {% if user.settings.defaultZoom==7 %}selected {% endif %}value="7">7</option>
<option {% if user.settings.defaultZoom==8 %}selected {% endif %}value="8">8</option>
<option {% if user.settings.defaultZoom==9 %}selected {% endif %}value="9">9</option>
<option {% if user.settings.defaultZoom==10 %}selected {% endif %}value="10">10 City</option>
<option {% if user.settings.defaultZoom==11 %}selected {% endif %}value="11">11</option>
<option {% if user.settings.defaultZoom==12 %}selected {% endif %}value="12">12</option>
<option {% if user.settings.defaultZoom==13 %}selected {% endif %}value="13">13</option>
<option {% if user.settings.defaultZoom==14 %}selected {% endif %}value="14">14</option>
<option {% if user.settings.defaultZoom==15 %}selected {% endif %}value="15">15 Streets</option>
<option {% if user.settings.defaultZoom==16 %}selected {% endif %}value="16">16</option>
<option {% if user.settings.defaultZoom==17 %}selected {% endif %}value="17">17</option>
<option {% if user.settings.defaultZoom==18 %}selected {% endif %}value="18">18</option>
<option {% if user.settings.defaultZoom==19 %}selected {% endif %}value="19">19</option>
<option {% if user.settings.defaultZoom==20 %}selected {% endif %}value="20">20 Buildings</option>
</select>
</div>
</div>
<div id='showStreet' class='form-group col-xs-12' title="{% if not user.isPro %}PRO ONLY! {% endif %}Shows a Google street view image at or near your current location, oriented in the direction of travel. ">
<label class='control-label col-xs-6 col-sm-4 col-lg-3' for="showStreet">Show street view{% if not user.isPro %} <span class='red'>(PRO)</span><br>{% endif %}</label>
<div class='input-group col-xs-6 col-sm-8 col-lg-9'>
<input class='form-control' name="showStreet" type="checkbox" {% if not user.isPro %}disabled{% elif user.settings.showStreetview %}checked{% else %}{% endif %}><br>
<div id='showScale' class='form-group' title="Shows a scale on the map.">
<label for="showScale">Show scale</label>
<input name="showScale" type="checkbox" {% if user.settings.showScale %}checked{% endif %}>
</div>
</div>
<div id='delete' class='form-group col-xs-12'>
<a class='btn red col-xs-5 col-md-3' style='margin-bottom:5px;float:right;' onclick="deleteAccount()">Delete account</a>
</div>
<div id='submit' class='form-group col-xs-12 flexbox' style="padding:0 0 60px">
<input class='btn yellow' style="width:50%; background:#333" type="submit" value="Save">
<a href="#" class='btn' style="width:50%; background:#333">cancel</a>
<div id='showSpeed' class='form-group' title="{% if not user.isPro %}PRO ONLY! {% endif %}Shows a spedometer on the map.">
<label for="showSpeed">Show speed{% if not user.isPro %} <span class='red'>(PRO)</span>{% endif %}</label>
<input name="showSpeed" type="checkbox" {% if not user.isPro %}disabled {% elif user.settings.showSpeed %}checked{% else %}{% endif %}>
</div>
<div id='showAltitude' class='form-group' title="{% if not user.isPro %}PRO ONLY! {% endif %}Shows the current elevation on the map. ">
<label for="showAlt">Show altitude{% if not user.isPro %} <span class='red'>(PRO)</span>{% endif %}</label>
<input name="showAlt" type="checkbox" {% if not user.isPro %}disabled {% elif user.settings.showAlt %}checked{% else %}{% endif %}>
</div>
<div id='showStreet' class='form-group' title="{% if not user.isPro %}PRO ONLY! {% endif %}Shows a Google street view image at or near your current location, oriented in the direction of travel. ">
<label for="showStreet">Show street view{% if not user.isPro %} <span class='red'>(PRO)</span>{% endif %}</label>
<input name="showStreet" type="checkbox" {% if not user.isPro %}disabled{% elif user.settings.showStreetview %}checked{% else %}{% endif %}>
</div>
<div id='submit-group' class='form-group flexbox'>
<input class='main btn' type="submit" value="Save" title="Click here to save your changes. ">
<a href="#" class='btn' title="Click here to discard your changes. ">cancel</a>
</div>
</form>
{% if not user.isPro %}<p style="clear:both">Want to try <a href="/pro">Tracman Pro</a>? It's free during beta testing. </p>{% endif %}
{% if not user.isPro %}<p style="clear:both">Want to try <a href="/settings/pro">Tracman Pro</a>? It's free during beta testing. </p>{% endif %}
<p style="clear:both">Would you like to <a href="https://github.com/Tracman-org/Server/issues/new">submit a suggestion or bug report</a>? </p>
</section>
{% endblock %}
{% endblock %}
{% block javascript %}
{{super()}}
<script type="text/javascript" src="/static/js/.settings.min.js"></script>
{% endblock %}

View File

@ -1,14 +1,30 @@
<!doctype html>
<!--
Tracman: GPS tracking service in node.js
Copyright © 2017 Keith Irwin
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
(A full copy of this programs license is available at https://github.com/Tracman-org/Server/blob/master/LICENSE.md)
-->
<html>
<head>
{% block head %}
<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,33 +41,29 @@
<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.min.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>
<script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.min.js"></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 %}
{% if not noHeader %}<link href="/static/css/.header.min.css" rel="stylesheet">{% endif %}
{% if not noFooter %}<link href="/static/css/.footer.min.css" rel="stylesheet">{% endif %}
</head>
<body>
<noscript>
<div class='header alert alert-danger alert-dismissible'>
<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>
</noscript>
{% 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);
@ -60,19 +72,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

@ -4,13 +4,13 @@
<br>Design by <a href="http://boag.online/blog/maglev-free-responsive-website-template">Fraser Boag</a>. </p>
</div>
<div class='right'>
Share:
<a href="https://www.facebook.com/sharer/sharer.php?u=https://tracman.org/" target="_blank"><i class="fa fa-facebook"></i></a>
<a href="https://twitter.com/home?status=Show%20your%20location%20to%20friends%20in%20realtime%20at%20https://tracman.org/" target="_blank"><i class="fa fa-twitter"></i></a>
<a href="https://www.reddit.com/submit?title=Show%20your%20location%20to%20friends%20in%20realtime%20at%20&url=https://tracman.org/" target="_blank"><i class="fa fa-reddit-alien"></i></a>
<br>Contribute:
Contribute:
<a href="https://github.com/Tracman-org/Server"><i class="fa fa-github"></i></a>
<a href="https://cash.me/$KeithIrwin"><i class="fa fa-dollar"></i></a>
<a href="bitcoin:14VN8GzWQPssWQherCE5XNGBWzy3eCDn74?label=tracman"><i class="fa fa-btc"></i></a>
<br>
<a href="/privacy">Privacy Policy</a> ▪️ <a href="/terms">Terms of Service</a>
</div>
</footer>
</footer>
<script type="text/javascript" src="/static/js/.footer.min.js"></script>

View File

@ -1,44 +1,59 @@
<script src="/static/js/header.js"></script>
<header class='shadow'>
<header>
<!-- Logo -->
<a href="/"><span class='logo'><img class='icon' src="/static/img/style/logo-28.png" alt="+">Tracman</span></a>
<!-- Hamburger -->
<div class='hamburger hamburger--slider' aria-label="Menu" aria-controls="navigation">
<div class='hamburger-box'>
<div class='hamburger-inner'></div>
</div>
</div>
<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>
<!-- Navigation -->
<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 %}
{% endif %}
<li><a href="/">About</a></li>
<li><a href="/help">Help</a></li>
{% if user %}
<li><a href="/logout">Logout</a></li>
{% else %}
<li><a href="/map/keith">Demo</a></li>
<li><a href="/login#login">Login</a></li>
<li><a href="/login#signup">Join</a></li>
{% endif %}
</ul></nav>
</header>
<!-- Sitewide notificaton -->
<!--<div class='header alert alert-warning alert-dismissible'>
<strong>Whoops!</strong> Tracman down right now. It will be fixed in a few days.
<a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a>
</div>-->
{% if error %}
<div class='header alert alert-danger alert-dismissible'>
<strong><i class="fa fa-exclamation-circle"></i> ERROR:</strong> {{error|safe}}
<a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a>
<!-- Flash messages -->
<noscript>
<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>
{% elif success %}
<div class='header alert alert-success alert-dismissible'>
</noscript>
{% for danger in dangers %}
<div class='alert alert-header alert-danger alert-dismissible shadow'>
<strong><i class="fa fa-exclamation-circle"></i> ERROR:</strong> {{ danger | safe }}
<a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a>
</div>
{% endfor %}
{% for warning in warnings %}
<div class='alert alert-header alert-warning alert-dismissible shadow'>
<strong><i class="fa fa-exclamation-circle"></i> Hey!</strong> {{ warning | safe }}
<a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a>
</div>
{% endfor %}
{% for success in successes %}
<div class='alert alert-header alert-success alert-dismissible shadow'>
<strong><i class="fa fa-check-circle"></i> Success!</strong> {{ success | safe }}
<a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a>
<a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a>
</div>
{% endif %}
{% endfor %}
<script type="text/javascript" src="/static/js/.header.min.js"></script>

12
views/terms.html Normal file
View File

@ -0,0 +1,12 @@
{% extends 'templates/base.html' %}
{% block title %}{{super()}} | Terms of Service{% endblock %}
{% block main %}
<section class='container'>
<h2>Terms of Service</h2>
<p>The terms of service haven't been written yet. (<a href="https://github.com/Tracman-org/Server/issues/48">#48</a>)</p>
</section>
{% endblock %}