commit
6865ac52a0
|
@ -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.*
|
||||
|
|
39
LICENSE.md
39
LICENSE.md
|
@ -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>.
|
||||
|
|
13
PRIVACY.md
13
PRIVACY.md
|
@ -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.
|
63
README.md
63
README.md
|
@ -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/)>.
|
108
config/auth.js
108
config/auth.js
|
@ -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);
|
||||
}
|
||||
});
|
||||
}));
|
|
@ -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'
|
||||
|
||||
};
|
|
@ -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}>`;
|
||||
}
|
||||
|
||||
};
|
|
@ -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
|
||||
}
|
||||
|
||||
};
|
|
@ -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)
|
||||
};
|
|
@ -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);
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
|
@ -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 );
|
||||
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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); });
|
||||
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"verbose": true,
|
||||
"ext": "html, js, json, css",
|
||||
"events": {
|
||||
"start": "npm run minify"
|
||||
}
|
||||
}
|
25
package.json
25
package.json
|
@ -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
189
server.js
|
@ -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;
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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(); });
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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(){});
|
||||
});
|
||||
|
||||
});
|
|
@ -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> 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');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
|
@ -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)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
|
@ -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
208
test.js
|
@ -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){
|
||||
|
||||
// });
|
||||
|
||||
});
|
||||
|
|
|
@ -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 */ {
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
106
views/help.html
106
views/help.html
|
@ -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/>your-slug<</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 %}
|
|
@ -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. Then send your friends a link with a map showing your live location. </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. </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¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted">donate</a> if you want to help with server expenses. </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. There's also has an android app which can run in the background. 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. </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. </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. </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. It'll look <a href="/map/keith">like this</a>. </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. Bam, now they know where you are. </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. </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. You can also show speed, altitude, and streetview. </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. </p>
|
||||
<p>Also keep in mind that publishing your location online could be a bad idea. </p>
|
||||
<p>I assume no responsibilities. </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. Things may break. </h3>
|
||||
<p>You will need a google account to join tracman and log in. </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 %}
|
||||
|
|
|
@ -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 © 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 %}
|
|
@ -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 %}
|
450
views/map.html
450
views/map.html
|
@ -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> 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> Track').tooltip('hide');
|
||||
navigator.geolocation.clearWatch(wpid);
|
||||
wpid = undefined;
|
||||
// Start tracking
|
||||
} else {
|
||||
$('#controls > .track').html('<i class="fa fa-crosshairs fa-spin"></i> 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> 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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 %}
|
Loading…
Reference in New Issue