Fix merge conflicts

master
Keith Irwin 2017-04-14 01:12:59 -04:00
commit 5b16e19c86
No known key found for this signature in database
GPG Key ID: 378933C743E2BBC0
47 changed files with 1971 additions and 7337 deletions

49
.gitignore vendored Normal file
View File

@ -0,0 +1,49 @@
# npm packages
node_modules
# Secret stuff
config/env*
!config/env-sample.js
# Minified static files (can be built with `npm run minify`)
static/**/*.min.*
# Ignore docs files
_gh_pages
_site
# Numerous always-ignore extensions
*.diff
*.err
*.orig
*.log
*.rej
*.swo
*.swp
*.zip
*.vi
*~
# OS or Editor folders
.DS_Store
._*
Thumbs.db
.cache
.project
.settings
.tmproj
*.esproj
nbproject
*.sublime-project
*.sublime-workspace
.idea
.c9
c9d
# Komodo
*.komodoproject
.komodotools
# grunt-html-validation
validation-status.json
validation-report.json

View File

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

View File

@ -1,13 +0,0 @@
# Privacy Policy for Tracman 0.3.1
In lieu of legalease, which I don't speak, here is a quick rundown of what Tracman does with your data (such as location).
## Location history
Your location is saved on the database as long as you have it "set" or "tracking". If you "clear" the data, it will be deleted from the database too. This doesn't mean all copies are destroyed. Our servers keep occasional backups, and caches could exist on other servers (google index, wayback archive, etc).
This means that all public access to your location is essentially deleted when you clear it. But anyone could record your location while it's publicly available and rebroadcast it. Tracman doesn't store location histories (except as mentioned above), but histories may exist elsewhere! If you have (or plan to have) trouble with the law, don't use Tracman. Authorities have easy access to those histories.
## Email addresses
Tracman stores email addresses so we can contact users for important stuff (urgent security updates, deletion requests, lost passwords). We will never subscribe you to anything else by default.

View File

@ -1,7 +1,7 @@
# Tracman
###### v 0.5.1
node.js application to display a map with user's location.
node.js application to display a map with user's location.
## Installation
@ -9,57 +9,33 @@ node.js application to display a map with user's location.
$ git clone https://github.com/Tracman-org/Server.git && (cd Server && exec npm install)
```
You will need to set up a configuration file at `config/env.js`. It should contain the following information:
```javascript
'use strict';
module.exports = {
env: 'development', // or 'production'
// Random strings to prevent hijacking
session: 'this is a secret',
cookie: 'shhhhh',
// Client IDs for authentication
googleClientId: '############-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com',
googleClientSecret: 'XXXXXXXXX_XXXXXXXXXXXXXX',
// A google maps API
googleMapsAPI: 'XXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXX',
// Location of your mongoDB
mongoSetup: 'mongodb://localhost/tracman',
// URL and port where Tracman will be run.
url: 'http://localhost:8080',
port: 8080
};
```
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).
You will need to set up a configuration file at `config/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).
## Running
```sh
$ npm start
$ npm run minify && npm start
```
Or with [nodemon](https://nodemon.io/):
or, using [nodemon](https://nodemon.io/):
```sh
$ npm dev
$ npm run nodemon
```
## 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).
## Changelog
#### v 0.5.1
#### v0.6.0
* Added more login options
* Replaced some callbacks with promises
* Minified static files
#### v0.5.1
* Fixed broken controls
@ -67,7 +43,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
@ -95,3 +71,17 @@ Tracman will be updated according to [this branching model](http://nvie.com/post
* Unified map and dashboard UI
* Security updates
* New admin UI
## License
###### see [LICENSE.md](https://github.com/Tracman-org/Server/blob/master/LICENSE.md)
Tracman: GPS tracking service in node.js
Copyright © 2017 [Keith Irwin](https://keithirwin.us/)
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <[http://www.gnu.org/licenses/](http://www.gnu.org/licenses/)>.

View File

@ -1,108 +1,251 @@
'use strict';
const passport = require('passport'),
const
mw = require('./middleware.js'),
mail = require('./mail.js'),
User = require('./models.js').user,
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');
env = require('./env.js');
passport.use(new GoogleStrategy({
clientID: secret.googleClientId,
clientSecret: secret.googleClientSecret,
callbackURL: secret.url+'/auth/google/callback',
passReqToCallback: true
}, function(req, accessToken, refreshToken, profile, done) {
module.exports = (app, passport) => {
// Methods for success and failure
const
loginOutcome = {
failureRedirect: '/login',
failureFlash: true
},
connectOutcome = {
failureRedirect: '/settings',
failureFlash: true
},
loginCallback = (req,res)=>{
res.redirect( req.session.next || '/map' );
};
// Check for user
User.findOne({googleID: profile.id}, function(err, user){
// Login/-out
app.route('/login')
.get( (req,res)=>{
if (req.isAuthenticated()){ loginCallback(); }
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.get('/signup', (req,res)=>{
res.redirect('/login#signup');
})
.post('/signup', (req,res,next)=>{
// 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;
// Send token and alert user
function sendToken(user){
// 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); }
// Create a password token
user.createToken((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>`)
}).catch((err)=>{
mw.throwErr(err,req);
res.redirect('/login#signup');
}).then(()=>{
req.flash('success', `An email has been sent to <u>${user.email}</u>. Check your inbox to complete your registration. `);
res.redirect('/login');
});
})(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 }); }
}
// Check if somebody already has that email
User.findOne({'email':req.body.email}, (err,user)=>{
if (err){ mw.throwErr(err,req); }
// 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
var generateSlug = new Promise((resolve,reject) => {
(function checkSlug(s,cb){
User.findOne({slug:s})
.catch((err)=>{
mw.throwErr(err,req);
})
.then((existingUser)=>{
// Slug in use: generate a random one and retry
if (existingUser){
s = '';
while (s.length<6) {
s+='abcdefghijkmnpqrtuvwxy346789'.charAt(Math.floor(Math.random()*28));
}
checkSlug(s,cb);
}
// Unique slug: proceed
else { cb(s); }
});
})(user.slug, (newSlug)=>{
user.slug = newSlug;
resolve();
});
});
// Generate sk32
var generateSk32 = new Promise((resolve,reject) => {
crypto.randomBytes(32, (err,buf)=>{
if (err) { mw.throwErr(err,req); }
user.sk32 = buf.toString('hex');
resolve();
});
});
// Save user and send the token by email
Promise.all([generateSlug, generateSk32])
.catch(err => {
mw.throwErr(err,req);
}).then(() => {
user.save( (err)=>{
if (err){ mw.throwErr(err,req); }
sendToken(user);
});
});
}
});
});
// Forgot password
app.route('/login/forgot')
.all( (req,res,next)=>{
if (req.isAuthenticated()){ loginCallback(); }
else { next(); }
})
.get( (req,res,next)=>{
res.render('forgot');
})
.post( (req,res,next)=>{
//TODO: Validate and sanitize email
// req.assert('email', 'Please enter a valid email address.').isEmail();
// req.sanitize('email').normalizeEmail({ remove_dots: false });
User.findOne( {'email':req.body.email}, (err,user)=>{
if (err){ mw.throwErr(err); }
// 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 exists
else {
// Create reset token
user.createToken( (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);
});
});
}
});
}
});
}));
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);
});
// Social
app.get('/login/:service', (req,res,next)=>{
let service = req.params.service,
sendParams = (service==='google')? {scope:['profile']} : null;
// Social login
if (!req.user) {
passport.authenticate(service, sendParams)(req,res,next);
}
// Connect social account
else if (!req.user.auth[service]) {
passport.authorize(service, sendParams)(req,res,next);
}
// Disconnect social account
else {
req.user.auth[service] = undefined;
req.user.save()
.catch((err)=>{
mw.throwErr(err,req);
res.redirect('/settings');
}).then(()=>{
req.flash('success', `${mw.capitalize(service)} account disconnected. `);
res.redirect('/settings');
});
}
});
}));
app.get('/login/:service/cb', (req,res,next)=>{
var service = req.params.service;
if (!req.user) {
passport.authenticate(service, loginOutcome)(req,res,next);
} else {
req.flash('success', `${mw.capitalize(service)} account connected. `);
req.session.next = '/settings';
passport.authenticate(service, connectOutcome)(req,res,next);
}
}, loginCallback);
// Android auth
//TODO: See if there's a better method
app.get('/auth/google/idtoken', passport.authenticate('google-id-token'), (req,res)=>{
if (!req.user){ res.sendStatus(401); }
else { res.send(req.user); }
} );
};

31
config/env-sample.js Normal file
View File

@ -0,0 +1,31 @@
'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',
// URL and port where this will run
url: 'https://localhost:8080',
port: 8080,
// OAuth API keys
facebookAppId: 'XXXXXXXXXXXXXXXX',
facebookAppSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
twitterConsumerKey: 'XXXXXXXXXXXXXXXXXXXXXXXXX',
twitterConsumerSecret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
googleClientId: '############-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com',
googleClientSecret: 'XXXXXXXXX_XXXXXXXXXXXXXX',
// Google maps API key
googleMapsAPI: 'XXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXX',
};

43
config/mail.js Normal file
View File

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

View File

@ -1,39 +1,37 @@
'use strict';
const secret = require('./secrets.js');
var throwErr = function(req,err){
console.error('middleware.js:5 '+typeof err);
console.error('Middleware error:'+err+'\nfor request:\n'+req);
if (secret.env==='production') {
req.flash('danger', 'An error occured. <br>Would you like to <a href="https://github.com/Tracman-org/Server/issues/new">report it</a>?');
} else { // development
req.flash('danger', err);
}
};
var ensureAuth = function(req,res,next){
if (req.isAuthenticated()) { return next(); }
else { res.redirect('/login'); }
};
var ensureAdmin = function(req,res,next){
ensureAuth(req,res,function(){
if (req.user.isAdmin){ return next(); }
else { next(); }
//TODO: test this by logging in as !isAdmin and go to /admin
// else if (!res.headersSent) { // 404 to users (not admin)
// var err = new Error('404: Not found: '+req.url);
// err.status = 404;
// res.render('error.html', {
// code: err.status
// });
// }
});
};
const env = require('./env.js');
module.exports = {
throwErr,
ensureAuth,
ensureAdmin
// Throw error
throwErr: (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 { res.sendStatus(401); }
//TODO: test this by logging in as !isAdmin and go to /admin
}
};

89
config/models.js Normal file
View File

@ -0,0 +1,89 @@
'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, required:true},
slug: {type:String, required:true, unique:true},
auth: {
password: String,
passToken: String,
tokenExpires: 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, required:true, default:'standard'},
defaultMap: {type:String, required:true, default:'road'},
defaultZoom: {type:Number, required:true, default:11},
showSpeed: {type:Boolean, required:true, default:false},
showTemp: {type:Boolean, required:true, default:false},
showAlt: {type:Boolean, required:true, default:false},
showStreetview: {type:Boolean, required:true, default:false}
},
last: {
time: Date,
lat: {type:Number, required:true, default:0},
lon: {type:Number, required:true, default:0},
dir: {type:Number, required:true, default:0},
alt: {type:Number, required:true, default:0},
spd: {type:Number, required:true, default:0}
},
sk32: {type:String, required:true, unique:true}
}).plugin(unique);
/* User methods */ {
// Generate hash for new password
userSchema.methods.generateHash = function(password,next){
bcrypt.genSalt(8, (err,salt)=>{
if (err){ return next(err); }
bcrypt.hash(password, salt, null, next);
});
};
// Create password reset token
userSchema.methods.createToken = function(next){
var user = this;
if ( user.auth.tokenExpires <= Date.now() ){
// Reuse old token, resetting clock
user.auth.tokenExpires = Date.now() + 3600000; // 1 hour
user.save();
return next(null,user.auth.passToken);
} else {
// Create new token
crypto.randomBytes(16, (err,buf)=>{
if (err){ next(err,null); }
else {
user.auth.passToken = buf.toString('hex');
user.auth.tokenExpires = Date.now() + 3600000; // 1 hour
user.save();
return next(null,user.auth.passToken);
}
});
}
};
// Check for valid password
userSchema.methods.validPassword = function(password,next){
bcrypt.compare(password, this.auth.password, next);
};
}
module.exports = {
'user': mongoose.model('User', userSchema)
};

View File

@ -1,35 +0,0 @@
'use strict';
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {type:String, required:true},
email: String,
slug: {type:String, required:true, unique:true},
requestId: String,
isAdmin: {type:Boolean, required:true, default:false},
isPro: {type:Boolean, required:true, default:false},
created: Date,
lastLogin: Date,
googleID: {type:Number, unique:true},
settings: {
units: {type:String, default:'standard'},
defaultMap: {type:String, default:'road'},
defaultZoom: {type:Number, default:11},
showSpeed: {type:Boolean, default:false},
showTemp: {type:Boolean, default:false},
showAlt: {type:Boolean, default:false},
showStreetview: {type:Boolean, default:false}
},
last: {
time: Date,
lat: {type:Number, default:0},
lon: {type:Number, default:0},
dir: {type:Number, default:0},
alt: {type:Number, default:0},
spd: {type:Number, default:0}
},
sk32: {type:String, required:true, unique:true}
});
module.exports = mongoose.model('User', userSchema);

159
config/passport.js Normal file
View File

@ -0,0 +1,159 @@
'use strict';
const
LocalStrategy = require('passport-local').Strategy,
GoogleStrategy = require('passport-google-oauth20').Strategy,
FacebookStrategy = require('passport-facebook').Strategy,
TwitterStrategy = require('passport-twitter').Strategy,
env = require('./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)=>{
User.findOne( {'email':email}, (err,user)=>{
if (err){ return done(err); }
// No user with that email
if (!user) {
return done( null, false, req.flash('danger','Incorrect email or password.') );
}
// User exists
else {
// Check password
user.validPassword( password, (err,res)=>{
if (err){ return done(err); }
// Password incorrect
if (!res) {
return done( null, false, req.flash('danger','Incorrect email or password.') );
}
// Successful login
else {
user.lastLogin = Date.now();
user.save();
return done(null,user);
}
} );
}
} );
}
));
// Social login
function socialLogin(req, service, profileId, done) {
// Log in
if (!req.user) {
// console.log(`Logging in with ${service}.`);
var query = {};
query['auth.'+service] = profileId;
User.findOne(query, (err,user)=>{
if (err){ return done(err); }
// Can't find user
else if (!user){
// Lazy update from old googleId field
if (service==='google') {
User.findOne( {'googleID':parseInt(profileId)}, (err,user)=>{
if (err){ return done(err); }
if (user) {
user.auth.google = profileId;
user.googleId = null;
user.save( (err)=>{
if (err){ mw.throwErr(err,req); }
else { console.info(`🗂️ Lazily updated schema for ${user.name}.`); }
return done(null, user);
} );
} else {
req.flash('danger',`There's no user for that ${service} account. `);
return done();
}
} );
}
// No googleId either
else {
req.flash('danger',`There's no user for that ${service} account. `);
return done();
}
}
// Successfull social login
else {
// console.log(`Found user: ${user}`);
return done(null, user);
}
});
}
// Connect account
else {
// console.log(`Connecting ${service} account.`);
req.user.auth[service] = profileId;
req.user.save( (err)=>{
if (err){ return done(err); }
else { return done(null, req.user); }
} );
}
}
// 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);
}
));
// 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);
}
));
// 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);
}
));
return passport;
};

View File

@ -2,52 +2,35 @@
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})
.catch( (err)=>{
mw.throwErr(err);
}).then( (found)=>{
res.render('admin', {
noFooter: '1',
users: found
});
});
} )
.post( (req,res,next)=>{
if (req.body.delete) {
User.findOneAndRemove({'_id':req.body.delete}, function(err,user){
User.findOneAndRemove( {'_id':req.body.delete}, (err,user)=>{
if (err){ req.flash('error', err.message); }
else { req.flash('success', '<i>'+user.name+'</i> deleted.'); }
res.redirect('/admin#users');
});
} else { console.log('ERROR! POST without action sent. '); next(); }
});
} );
} else { console.error(new Error('POST without action sent. ')); next(); }
} );
module.exports = router;

View File

@ -1,34 +0,0 @@
'use strict';
const router = require('express').Router(),
passport = require('passport');
// Routes
router.get('/login', function(req,res){
res.redirect('/auth/google');
});
router.get('/logout', function(req,res){
req.logout(); // Needs to clear cookies?
req.flash('success', 'You have been logged out. ');
res.redirect('/');
});
// Web app auth
router.get('/auth/google', passport.authenticate('google', { scope: [
'https://www.googleapis.com/auth/plus.login',
'https://www.googleapis.com/auth/plus.profile.emails.read'
] }));
router.get('/auth/google/callback', passport.authenticate('google', {
failureRedirect: '/',
failureFlash: true,
successRedirect: '/',
successFlash: true
} ));
// Android auth
router.get('/auth/google/idtoken', passport.authenticate('google-id-token'), function (req,res) {
if (!req.user) { res.sendStatus(401); }
else { res.send(req.user); }
} );
module.exports = router;

View File

@ -1,99 +1,61 @@
'use strict';
const slug = require('slug'),
xss = require('xss'),
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'),
User = require('../models.js').user;
// Index
router.get('/', function(req,res,next) {
res.render('index.html');
router.get('/', (req,res,next)=>{
res.render('index');
});
// Settings
router.route('/settings').all(mw.ensureAuth, function(req,res,next){
next();
})
// Get settings form
.get(function(req,res,next){
User.findById(req.session.passport.user, function(err,user){
if (err){ console.log('Error finding settings for user:',err); mw.throwErr(req,err); }
res.render('settings.html');
});
})
// Set new settings
.post(function(req,res,next){
User.findByIdAndUpdate(req.session.passport.user, {$set:{
name: xss(req.body.name),
slug: slug(xss(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('/settings');
});
})
// Delete user account
.delete(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('/');
}
}
);
});
// Tracman pro
router.route('/pro').all(mw.ensureAuth, function(req,res,next){
next();
})
// Get info about pro
.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'); }
});
})
// Join Tracman pro
.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');
}
);
});
// Help
router.route('/help').get(mw.ensureAuth, function(req,res){
res.render('help.html');
});
router.get('/help', mw.ensureAuth, (req,res)=>{
res.render('help');
});
// Terms of Service
router.get('/terms', function(req,res){
res.render('terms.html');
// Terms of Service and Privacy Policy
router.get('/terms', (req,res)=>{
res.render('terms');
})
.get('/privacy', (req,res)=>{
res.render('privacy');
});
// robots.txt
router.get('/robots.txt', (req,res)=>{
res.type('text/plain');
res.send("User-agent: *\n"+
"Disallow: /map/*\n"
);
});
// favicon.ico
router.get('/favicon.ico', (req,res)=>{
res.redirect('/static/img/icon/by/16-32-48.ico');
});
// Endpoint to validate forms
router.get('/validate', (req,res)=>{
if (req.query.slug) { // validate unique slug
User.findOne( {slug:slug(req.query.slug)}, (err,existingUser)=>{
if (err) { console.log('/validate error:',err); }
if (existingUser && existingUser.id!==req.user) { res.sendStatus(400); }
else { res.sendStatus(200); }
} );
}
});
// Link to androidapp in play store
router.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
router.get('/ios', (req,res)=>{
res.sendStatus(404);
//TODO: Add link to info about why there's no ios app
});
module.exports = router;

View File

@ -1,17 +1,23 @@
'use strict';
//TODO: Use promises
const router = require('express').Router(),
mw = require('../middleware.js'),
secrets = require('../secrets.js'),
User = require('../models/user.js');
env = require('../env.js'),
User = require('../models.js').user;
// Redirect to real slug
router.get('/', mw.ensureAuth, (req,res)=>{
res.redirect(`/map/${req.user.slug}`);
});
// Show map
router.get('/:slug?', function(req,res,next){
router.get('/:slug?', (req,res,next)=>{
var mapuser='', user='', cbc=0;
// Confirm sucessful queries
function checkQuery(err,found) {
if (err){ mw.throwErr(req,err); }
if (err){ mw.throwErr(err,req); }
if (found){ return found; }
}
@ -24,7 +30,7 @@ router.get('/:slug?', function(req,res,next){
// QUERIES
// Get logged in user -> user
if (req.isAuthenticated()) {
User.findById(req.session.passport.user, function(err, found) {
User.findById(req.user, function(err, found) {
user = checkQuery(err,found);
checkCBC();
});
@ -44,9 +50,9 @@ router.get('/:slug?', function(req,res,next){
res.redirect('/');
} else {
if (user && !mapuser) { mapuser = user; }
res.render('map.html', {
res.render('map', {
mapuser: mapuser,
mapApi: secrets.mapAPI,
mapApi: env.googleMapsAPI,
user: user,
noFooter: '1',
noHeader: (req.query.noheader)?req.query.noheader.match(/\d/)[0]:'',

View File

@ -1,36 +0,0 @@
'use strict';
const router = require('express').Router(),
slug = require('slug'),
User = require('../models/user.js');
// robots.txt
router.get('/robots.txt', function(req,res){
res.type('text/plain');
res.send("User-agent: *\n"+
"Disallow: /map/*\n"
);
});
// favicon.ico
router.get('/favicon.ico', function(req,res){
res.redirect('/static/img/icon/by/16-32-48.ico');
});
// Endpoint to validate forms
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); }
});
}
});
// Link to android app in play store
router.get('/android', function(req,res){
res.redirect('https://play.google.com/store/apps/details?id=us.keithirwin.tracman');
});
module.exports = router;

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

@ -0,0 +1,182 @@
'use strict';
const slug = require('slug'),
xss = require('xss'),
mw = require('../middleware.js'),
User = require('../models.js').user,
mail = require('../mail.js'),
env = require('../env.js'),
router = require('express').Router();
// Settings form
router.route('/')
.all( mw.ensureAuth, (req,res,next)=>{
next();
} )
// Get settings form
.get( (req,res,next)=>{
User.findById( req.user, (err,user)=>{
if (err){ mw.throwErr(err,req); }
res.render('settings');
} );
} )
// Set new settings
.post( (req,res,next)=>{
User.findByIdAndUpdate(req.user, {$set:{
name: xss(req.body.name),
slug: slug(xss(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
}
}}, (err,user)=>{
if (err) {
mw.throwErr(err,req);
res.redirect('/settings');
}
else {
req.flash('success', 'Settings updated. ');
res.redirect('/settings');
}
});
} )
// Delete user account
.delete( (req,res,next)=>{
User.findByIdAndRemove( req.user, (err)=>{
if (err) {
mw.throwErr(err,req);
res.redirect('/settings');
} else {
req.flash('success', 'Your account has been deleted. ');
res.redirect('/');
}
} );
} );
// 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.createToken( (err,token)=>{
if (err){ next(err); }
// 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 in 1 hour. `),
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 in 1 hour. </p>`)
}).catch( err=>{
mw.throwErr(err,req);
res.redirect('/login#login');
}).then( ()=>{
// Alert user to check email.
req.flash('success',`An email has been sent to <u>${req.user.email}</u>. Check your inbox to complete your password change. `);
res.redirect('/login#login');
});
} );
} );
router.route('/password/:token')
// Check token
.all( (req,res,next)=>{
User
.findOne({'auth.passToken': req.params.token})
.where('auth.tokenExpires').gt(Date.now())
.catch((err)=>{
mw.throwErr(err,req);
})
.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();
}
});
} )
// Show password change form
.get( (req,res)=>{
res.render('password');
} )
.post( (req,res,next)=>{
//TODO: Validate password
// Delete token
res.locals.passwordUser.auth.passToken = undefined;
res.locals.passwordUser.auth.tokenExpires = undefined;
// Create hash
res.locals.passwordUser.generateHash( req.body.password, (err,hash)=>{
if (err){ mw.throwErr(err,req); }
else {
// Save new password to db
res.locals.passwordUser.auth.password = hash;
res.locals.passwordUser.save( (err)=>{
if (err){
mw.throwErr(err,req);
res.redirect('/login#signup');
}
else {
req.flash('success', 'Password set. You can use it to log in now. ');
res.redirect('/login#login');
}
});
}
} );
} );
// 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 }},
(err,user)=>{
if (err){ mw.throwErr(err,req); }
else { req.flash('success','You have been signed up for pro. '); }
res.redirect('/map');
}
);
} );
module.exports = router;

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

@ -0,0 +1,29 @@
'use strict';
const router = require('express').Router(),
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);
next();
});
})
.get('/password', (req,res)=>{
res.render('password');
});
module.exports = router;

View File

@ -1,7 +1,8 @@
'use strict';
// Imports
const User = require('./models/user.js');
const mw = require('./middleware.js'),
User = require('./models.js').user;
// Check for tracking clients
function checkForUsers(io, user) {
@ -9,9 +10,9 @@ 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){
if (Object.keys(io.sockets.connected).map( (id)=>{
return io.sockets.connected[id];
}).some( function(socket){
}).some( (socket)=>{
return socket.gets==user;
})) {
//console.log(`Activating updates for ${user}.`);
@ -26,56 +27,56 @@ 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){
/* 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) }
if (!loc.tok) { mw.throwErr(new Error(`⛔️ !loc.tok for loc: ${loc}`)) }
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); }
else {
User.findById(loc.usr, (err,user)=>{
if (err) { mw.throwErr(err); }
if (user) {
// Confirm sk32 token
if (loc.tok!=user.sk32) { console.log('loc.tok!=user.sk32 || ',loc.tok,'!=',user.sk32); }
if (loc.tok!=user.sk32) { mw.throwErr(new Error(`⛔️ loc.tok!=user.sk32\n\t${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),
@ -84,9 +85,9 @@ module.exports = {
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); }
});
user.save( (err)=>{
if (err) { mw.throwErr(err); }
} );
}
}
@ -96,7 +97,7 @@ module.exports = {
});
// 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,8 +109,8 @@ module.exports = {
});
// Log errors
socket.on('error', function(err){
console.log('Socket error! ',err);
socket.on('error', (err)=>{
mw.throwErr(err);
});
});

7
nodemon.json Normal file
View File

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

View File

@ -4,6 +4,7 @@
"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",
"cookie-parser": "^1.4.1",
@ -14,11 +15,17 @@
"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-google-id-token": "^0.4.0",
"passport-google-oauth2": "^0.1.6",
"passport-google-oauth20": "^1.0.0",
"passport-local": "^1.0.0",
"passport-twitter": "^1.0.4",
"slug": "^0.9.1",
"socket.io": "^1.4.4"
},
@ -30,6 +37,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"
@ -37,8 +45,8 @@
"scripts": {
"test": "mocha test.js",
"start": "node server.js",
"dev": "nodemon server.js",
"deploy": "ssh khp deploy-tracman",
"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": {

143
server.js
View File

@ -1,7 +1,7 @@
'use strict';
/* IMPORTS */
const
/* IMPORTS */
const
express = require('express'),
bodyParser = require('body-parser'),
cookieParser = require('cookie-parser'),
@ -11,7 +11,8 @@ const
passport = require('passport'),
flash = require('connect-flash'),
env = require('./config/env.js'),
User = require('./config/models/user.js'),
mw = require('./config/middleware.js'),
User = require('./config/models.js').user,
app = express(),
http = require('http').Server(app),
io = require('socket.io')(http),
@ -19,21 +20,36 @@ const
/* SETUP */ {
/* Database */ mongoose.connect(env.mongoSetup, {
server:{socketOptions:{
keepAlive:1, connectTimeoutMS:30000 }},
replset:{socketOptions:{
keepAlive:1, connectTimeoutMS:30000 }}
});
/* Templates */ nunjucks.configure(__dirname+'/views', {
autoescape: true,
express: app
});
/* 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 }}
}).catch((err)=>{
mw.throwErr(err);
}).then(()=>{
console.log(`💿 Mongoose connected to mongoDB`);
});
}
/* Templates */ {
nunjucks.configure(__dirname+'/views', {
autoescape: true,
express: app
});
app.set('view engine','html');
}
/* Session */ {
app.use(cookieParser(env.cookie));
// app.use(expressSession({
app.use(cookieSession({
cookie: {maxAge:60000},
secret: env.session,
@ -46,90 +62,86 @@ const
}));
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);
});
});
require('./config/auth.js')(app, passport);
}
/* Routes */ {
// Static files (keep this before setting default locals)
app.use('/static', express.static(__dirname+'/static'));
// Set default locals (keep this after static files)
app.get('/*', function(req,res,next){
// console.log(`Setting local variables for request to ${req.path}.`);
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
req.session.next = ( req.path.substring(0, req.path.indexOf('#')) || req.path )+'#';
// User account
res.locals.user = req.user;
// console.log(`User set as ${res.locals.user}. `);
// Flash messages
res.locals.successes = req.flash('success');
res.locals.dangers = req.flash('danger');
res.locals.warnings = req.flash('warning');
// console.log(`Flash messages set as:\nSuccesses: ${res.locals.successes}\nWarnings: ${res.locals.warnings}\nDangers: ${res.locals.dangers}`);
next();
});
} );
// Main routes
app.use('/',
require('./config/routes/index.js'),
require('./config/routes/auth.js'),
require('./config/routes/misc.js')
);
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'));
app.use( ['/map','/trac'], require('./config/routes/map.js') );
// Admin
app.use('/admin', require('./config/routes/admin.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('404 Not found: '+req.url);
err.status = 404;
next(err);
}
});
} );
// Handlers
if (env.env=='production') {
app.use(function(err,req,res,next) {
if (env.mode=='production') {
app.use( (err,req,res,next)=>{
if (res.headersSent) { return next(err); }
res.status(err.status||500);
res.render('error.html', {
res.render('error', {
code: err.status
});
});
} );
}
else /* Development */{
app.use(function(err,req,res,next) {
app.use( (err,req,res,next)=>{
console.log(err);
if (res.headersSent) { return next(err); }
res.status(err.status||500);
res.render('error.html', {
res.render('error', {
code: err.status,
message: err.message,
error: err
error: err.stack
});
});
} );
}
}
@ -141,23 +153,22 @@ const
/* RUNTIME */ {
console.log('🖥 Starting Tracman server...');
// Listen
http.listen(env.port, function(){
console.log(
'==========================================\n'+
'Listening at '+env.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){
User.find( {}, (err,users)=>{
if (err) { console.log(`DB error finding all users: ${err.message}`); }
users.forEach( function(user){
users.forEach( (user)=>{
sockets.checkForUsers( io, user.id );
});
});
});
}
module.exports = app;

View File

@ -1,106 +1,42 @@
/* Resets, Clears & Defaults */
*, *:after, *:before {
/* Global */
div, footer, .fa,
.container, .container:before, .container:after {
box-sizing: border-box;
}::-webkit-scrollbar {
width: 5vw;
min-width:10px;
max-width:40px;
}::-webkit-scrollbar-track {
background-color: #080808;
}::-webkit-scrollbar-thumb {
border-radius: .2vw;
background: #333;
}
body {
background-color: #080808;
color: #eee;
}
body, input, textarea {
padding: 0; margin: 0;
font-family: 'Open Sans', sans-serif;
font-size: 18px;
color: #eee;
font-weight: 600;
}
.flexbox {
width:100%;
display:flex;
justify-content:space-around;
body {
background-color: #080808;
}
.flexbox.stretch { justify-content:space-between; }
pre {
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
::-webkit-scrollbar {
width: 5vw;
min-width:10px;
max-width:40px;
}::-webkit-scrollbar-track {
background-color: #080808;
background-color: rgba(8,8,8,0);
}::-webkit-scrollbar-thumb {
border-radius: .2vw;
background: #333;
}
.dark pre {
-moz-box-shadow: 2px 2px 4px #000;
-webkit-box-shadow: 2px 2px 4px #000;
box-shadow: 2px 2px 4px #000;
background-color: rgba(255,255,255,.03);
color: #aaa;
padding: 1%;
border: 1px solid #ccc;
border-radius: .25rem;
::selection {
background: #999;
}
.dark .form-control:disabled, .dark .form-control:disabled {
background-color: rgba(255,255,255,0.1);
}
.input-group {
margin-bottom:30px;
}
input[type="checkbox"] {
margin: 8px 0;
}
.form-group#buttons {
width: 100%;
display: flex;
justify-content: space-around;
}
input[type="checkbox"] {
display: inline-block;
}
.help-block {margin-top:-20px;}
.alert {
z-index:10;
}
.alert-header {
position: relative;
top: 58px;
} .alert-header.alert-danger {
z-index: 103;
} .alert-header.alert-warning {
z-index: 102;
} .alert-header.alert-success {
z-index: 101;
}
.alert:not(.alert-dismissible) {
text-align: center;
}
.alert a {
color: inherit;
text-decoration: underline;
}
.alert a:hover {
color: inherit;
text-decoration: none;
::-moz-selection {
background: #999;
}
input:focus, textarea:focus {
outline: 0;
}
h1, h2, h3, p {
margin: 0 0 20px 0;
/* Elements */
h1, h2, h3 {
margin: 0 0 5% 0;
position: relative;
z-index: 6;
}
h1,h2,h3,h4 { font-weight: 600; }
h1, h2, h3, h4 { font-weight: 600; }
h1 {
font-size: 48px;
line-height: 46px; }
@ -109,35 +45,30 @@ h2 {
line-height: 36px; }
h3 { font-size: 28px; }
h4 { font-size: 20px; }
.red { color: #fb6e3d; }
.shadow {
-moz-box-shadow: .18vw .18vw .36vw #000;
-webkit-box-shadow: .18vw .18vw .36vw #000;
box-shadow: .18vw .18vw .36vw #000;
} .shadow:active {
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
p {
margin-top: 0;
margin-bottom: 10vh;
}
a {
color: #fbc93d;
text-decoration: none;
}
a:hover {
main a:hover:not(.btn) {
color: #fbc93d;
text-decoration: underline;
}
.light a {
color:#111;
a.underline {
text-decoration: underline;
}
.light a:hover {
color:#111;
a.underline:hover:not(.btn) {
text-decoration: none;
}
hr {
width: 90%;
margin: 10% auto;
}
img {
max-width: 100%;
}
@ -145,27 +76,48 @@ p img {
display: block;
margin: auto;
}
input[type="checkbox"] {
width: auto;
margin: 8px;
}
.with-errors {
color: #d9534f;
pre {
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
}
::selection {
background: #999;
.hide { display: none !important; }
.red, .red:hover { color: #fb6e3d !important; }
.yellow, .yellow:hover { color: #fbc93d !important; }
.shadow {
-moz-box-shadow: .18vw .18vw .36vw #000;
-webkit-box-shadow: .18vw .18vw .36vw #000;
box-shadow: .18vw .18vw .36vw #000;
} .shadow:active {
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
}
::-moz-selection {
background: #999;
}
/* End Resets, Clears & Defaults */
.container {
.inline { display: inline-block; }
.flex {
width: 100%;
display: flex;
justify-content: space-around;
}
.flex.stretch { justify-content: space-between; }
.left { float: left; }
.right { float: right; }
main {
top: 60px;
position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
overflow-y: auto;
}
.container {
padding-right: 5%;
padding-left: 5%;
width: 100%;
max-width: 1000px;
margin: 0 auto;
}
.container:after {
@ -174,55 +126,51 @@ input[type="checkbox"] {
clear: both;
}
section {
padding: 100px 0 50px;
padding: 10vh 0 5vh;
}
/* Buttons */
.btn {
text-decoration: none;
font-weight:600;
display: inline-block;
padding: 15px 30px;
transition: 200ms;
background: transparent;
transition: 100ms;
cursor: pointer;
-moz-box-shadow: 2px 2px 4px #000;
-webkit-box-shadow: 2px 2px 4px #000;
box-shadow: 2px 2px 4px #000;
}
.dark .btn {
color: #fff;
border: 1px solid #fff;
}
.dark .btn:hover:not(.disabled),
.dark .btn:active:not(.disabled),
.dark .btn:focus:not(.disabled) {
background: rgba(255,255,255,0.1);
}.dark.btn:active:not(.disabled) {
-moz-box-shadow: 0;
-webkit-box-shadow: 0;
box-shadow: 0;
}
.light .btn {
color: #222;
color: #eee;
border: 1px solid #666;
border-radius: .5vw;
} .btn:not(.disabled) {
-moz-box-shadow:
inset .11vw .18vw .52vw rgba(255,255,255,.2),
inset -.11vw -.18vw .52vw rgba(0,0,0,.4),
.11vw .18vw .52vw #000;
-webkit-box-shadow:
inset .11vw .18vw .52vw rgba(255,255,255,.2),
inset -.11vw -.18vw .52vw rgba(0,0,0,.4),
.18vw .18vw .36vw #000;
box-shadow:
inset .11vw .18vw .52vw rgba(255,255,255,.2),
inset -.11vw -.18vw .52vw rgba(0,0,0,.4),
.18vw .18vw .36vw #000;
} .btn:hover:not(.disabled) {
text-decoration: none;
border: 1px solid #222;
}
.light .btn:hover:not(.disabled),
.light .btn:active:not(.disabled),
.light .btn:focus:not(.disabled) {
background: rgba(0,0,0,0.1);
}
.btn.yellow {
color: #fbc93d;
background: rgba(255,255,255,0.2);
} .btn:active:not(.disabled) {
-moz-box-shadow:
inset .11vw .18vw .52vw rgba(0,0,0,.4),
inset -.11vw -.18vw .52vw rgba(255,255,255,.2);
-webkit-box-shadow:
inset .11vw .18vw .52vw rgba(0,0,0,.4),
inset -.11vw -.18vw .52vw rgba(255,255,255,.2);
box-shadow:
inset .11vw .18vw .52vw rgba(0,0,0,.4),
inset -.11vw -.18vw .52vw rgba(255,255,255,.2);
} .btn:focus:not(.disabled){
border: 1px solid #fbc93d;
}
.dark .btn.yellow:hover:not(.disabled),
.dark .btn.yellow:active:not(.disabled),
.dark .btn.yellow:focus:not(.disabled) {
background: rgba(251,201,61,0.1);
}
.btn.smaller {
padding: 10px 25px;
.btn.main {
color: #fbc93d;
}
.btn .fa {
margin-left: 10px;

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,13 @@
footer {
font-weight: 300;
width:100%;
overflow:auto;
width: 100%;
overflow: auto;
background: #111;
color: #ccc;
padding: 0 20px;
-moz-box-shadow: inset 0 .25vw 1vw #222;
-webkit-box-shadow: inset 0 .25vw 1vw #222;
box-shadow: inset 0 .25vw 1vw #222;
}
footer .left {
float: left;
@ -33,7 +36,6 @@ footer a .fa {
footer .fa a:hover, footer .fa a:focus {
color: inherit;
}
@media (max-width: 800px) {
footer {
padding: 0 10px;
@ -49,4 +51,4 @@ footer .fa a:hover, footer .fa a:focus {
footer .right {
padding-top: 0;
}
}
}

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

@ -0,0 +1,106 @@
form {
margin: auto;
max-width: 800px;
}
.form-group {
display: flex;
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 {
-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);
color: #eee;
background-color: #202020;
background-color: rgba(255,255,255,0.1);
padding: 1% 1.5%;
border: 1px solid #666;
border-radius: .3vw;
}
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 {
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;
-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-with-addon {
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;
-moz-box-shadow: inset 0 .18vw .25vw rgba(0,0,0,.5);
-webkit-box-shadow: inset 0 .18vw .25vw rgba(0,0,0,.5);
box-shadow: inset 0 .18vw .25vw rgba(0,0,0,.5);
}
::-webkit-input-placeholder {
color: #666;
}:-moz-placeholder {
color: #666;
opacity: 1;
}::-moz-placeholder {
color: #666;
opacity: 1;
}:-ms-input-placeholder {
color: #666;
}
form select {
-moz-box-shadow: inset 0 1px 6px rgba(255,255,255,.2),
inset -.11vw -.18vw .52vw rgba(0,0,0,.4);
-webkit-box-shadow: inset -.11vw -.18vw .52vw rgba(255,255,255,.2),
inset -.11vw -.18vw .52vw rgba(0,0,0,.4);
box-shadow: inset 0 .11vw .52vw rgba(255,255,255,.2),
inset 0 .11vw .52vw rgba(0,0,0,.4);
}
form select > option {
background: #222;
color: inherit;
}
form .radio {
min-width: 150px;
display: flex;
justify-content: space-between;
}
form input[type="checkbox"], form input[type="radio"] {
width: auto;
margin: 8px;
}
form input[type="checkbox"]:active, form input[type="radio"]:active,
form input[type="checkbox"]:focus, form input[type="radio"]:focus {
outline: 1px solid #fbc93d;
}
form .btn {
font-size: 1.5em;
}

View File

@ -1,3 +1,4 @@
/* Main */
header {
background: #222;
padding: 0;
@ -5,9 +6,12 @@ header {
top: 0; left: 0;
width: 100%;
z-index: 200;
} header a:hover, header a:focus {
color: #fbc93d;
}
/* Logo */
header .logo {
float: left;
font-family: 'Open Sans', sans-serif;
padding: 13px 23px;
@ -16,53 +20,43 @@ header .logo {
font-size: 22px;
line-height: 30px;
margin: 0;
}
header a:hover, header a:focus {
color: #fbc93d;
}
header .logo a {
} header .logo a {
color:inherit;
font:inherit;
text-decoration:inherit;
cursor: pointer;
}
header .logo img {
} header .logo img {
margin-right: 10px;
position: relative;
width:28px;
height:28px;
vertical-align: middle;
} header .logo:hover {
text-decoration: none;
background: rgba(255,255,255,0.1);
}
/* Navigation */
header nav {
float: right;
}
header nav ul {
} header nav ul {
padding: 0;
margin: 0;
}
header nav ul li {
} header nav ul li {
display: inline-block;
float: left;
}
header nav ul li a, header nav ul li span {
} header nav ul li a, header nav ul li span {
text-decoration:inherit;
display: inline-block;
padding: 15px 20px;
color: #fff;
transition: 200ms;
}
header nav ul li a:hover,
transition: 100ms;
} header nav ul li a:hover,
header nav ul li a:focus,
header nav ul li a.active,
header .logo:hover {
text-decoration: none;
background: rgba(255,255,255,0.1);
}
.alert.header {
position: relative;
border-radius: 0;
top: 58px;
width: 100%;
}
/* Hamburger */
header .hamburger {
display: none;
padding: 5px;
@ -70,18 +64,20 @@ header .hamburger {
transition-property: opacity, -webkit-filter;
transition-property: opacity, filter;
transition-property: opacity, filter, -webkit-filter;
transition-duration: 0.15s;
transition-timing-function: linear; }
.hamburger:hover {
opacity: 0.7; }
header .hamburger-box {
transition-duration: 150ms;
transition-timing-function: linear;
} header .hamburger:hover {
opacity: 0.7;
} header .hamburger-box {
width: 40px;
height: 24px;
position: relative; }
header .hamburger-inner {
position: relative;
} header .hamburger-inner {
top: 50%;
margin-top: -2px; }
header .hamburger-inner, header .hamburger-inner::before, header .hamburger-inner::after {
margin-top: -2px;
} header .hamburger-inner,
header .hamburger-inner::before,
header .hamburger-inner::after {
width: 40px;
height: 4px;
background-color: #fff;
@ -90,42 +86,47 @@ header .hamburger-inner {
transition-property: -webkit-transform;
transition-property: transform;
transition-property: transform, -webkit-transform;
transition-duration: 0.15s;
transition-timing-function: ease; }
header .hamburger-inner::before, header .hamburger-inner::after {
transition-duration: 150ms;
transition-timing-function: ease;
} header .hamburger-inner::before, header .hamburger-inner::after {
content: "";
display: block; }
header .hamburger-inner::before {
top: -10px; }
header .hamburger-inner::after {
bottom: -10px; }
header .hamburger--slider .hamburger-inner {
top: 0; }
header .hamburger--slider .hamburger-inner::before {
display: block;
} header .hamburger-inner::before {
top: -10px;
} header .hamburger-inner::after {
bottom: -10px;
} header .hamburger--slider .hamburger-inner {
top: 0;
} header .hamburger--slider .hamburger-inner::before {
top: 10px;
transition-property: opacity, -webkit-transform;
transition-property: transform, opacity;
transition-property: transform, opacity, -webkit-transform;
transition-timing-function: ease;
transition-duration: 0.2s; }
header .hamburger--slider .hamburger-inner::after {
top: 20px; }
header .hamburger--slider.is-active .hamburger-inner {
-webkit-transform: translate3d(0, 10px, 0) rotate(45deg);
transform: translate3d(0, 10px, 0) rotate(45deg); }
header .hamburger--slider.is-active .hamburger-inner::before {
transition-duration: 200ms;
} header .hamburger--slider .hamburger-inner::after {
top: 20px;
} header .hamburger--slider.is-active .hamburger-inner {
-webkit-transform: translate3d(0, 10px, 0) rotate(45deg);
-moz-transform: translate3d(0, 10px, 0) rotate(45deg);
-md-transform: translate3d(0, 10px, 0) rotate(45deg);
-o-transform: translate3d(0, 10px, 0) rotate(45deg);
transform: translate3d(0, 10px, 0) rotate(45deg);
} header .hamburger--slider.is-active .hamburger-inner::before {
-webkit-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
opacity: 0; }
header .hamburger--slider.is-active .hamburger-inner::after {
-moz-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
-ms-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
-o-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
opacity: 0;
} header .hamburger--slider.is-active .hamburger-inner::after {
-webkit-transform: translate3d(0, -20px, 0) rotate(-90deg);
transform: translate3d(0, -20px, 0) rotate(-90deg); }
-moz-transform: translate3d(0, -20px, 0) rotate(-90deg);
-ms-transform: translate3d(0, -20px, 0) rotate(-90deg);
-o-transform: translate3d(0, -20px, 0) rotate(-90deg);
transform: translate3d(0, -20px, 0) rotate(-90deg);
}
@media (max-width: 800px) {
header { padding:0; }
header nav ul li a { padding:15px; }
}
@media (max-width: 600px) {
@ -138,17 +139,14 @@ header .hamburger--slider.is-active .hamburger-inner {
width: 100%;
max-width: 300px;
background: #333;
transition: 200ms;
}
header nav.visible {
transition: 100ms;
} header nav.visible {
right: 0px;
}
header nav ul li {
} header nav ul li {
display: block;
float: none;
width: 100%;
}
header nav ul li a {
} header nav ul li a {
display: block;
width: 100%;
border-bottom: 1px solid rgba(255,255,255,0.1);
@ -161,3 +159,64 @@ header .hamburger--slider.is-active .hamburger-inner {
top: 13px;
}
}
/* Alerts */
.alert {
padding: 15px;
border: 1px solid transparent;
border-radius: 4px;
}
noscript .alert-danger {
z-index: 40;
}
.alert-danger {
z-index: 30;
color: #f2dede;
background-color: #a94442;
}
.alert-warning {
z-index: 20;
color: #fcf8e3;
background-color: #8a6d3b;
}
.alert-success {
z-index: 10;
color: #dff0d8;
background-color: #3c763d;
}
.alert.alert-header {
position: relative;
border-radius: 0;
top: 58px;
width: 100%;
}
.alert a {
z-index: 10;
color: inherit;
font-weight: bold;
text-decoration: underline;
}
.alert a:hover {
color: inherit;
text-decoration: none;
}
.alert h4 {
margin-top: 0;
color: inherit;
}
.alert > p,
.alert > ul {
margin-bottom: 0;
}
.alert > p + p {
margin-top: 5px;
}
.alert-dismissable {
padding-right: 35px;
}
.alert .close,
.alert-dismissible .close {
cursor: pointer;
float: right;
color: inherit;
}

View File

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

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

@ -0,0 +1,49 @@
section > .flex > div {
width: 50%;
padding: 0 2%;
}
form input {
width: 96%;
}
form input.btn,
form #social-login {
width: 100%;
}
#social-login .btn {
padding: 0;
font-size: 1.3em;
height: 60px;
text-align: center;
width: 60px;
margin: 0 3%;
color: #FFF;
} #social-login .btn .fa {
margin: 0;
position: relative;
padding-top: 20px;
} #social-login .btn.gp {
background: rgb(206,77,57);
} #social-login .btn.gp:hover {
background: rgb(251,122,102);
} #social-login .btn.fb {
background: rgb(48,88,145);
} #social-login .btn.fb:hover {
background: rgb(93,133,190);
} #social-login .btn.tw {
background: rgb(44,168,210);
} #social-login .btn.tw:hover {
background: rgb(89,213,255);
}
@media (max-width: 800px) {
section > .flex {
flex-direction: column;
}
section > .flex > div {
width: 100%;
}
hr.hide { display:block; }
}

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

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

View File

@ -1,3 +1,4 @@
/* global $ */
'use strict';
$(document).ready(function(){
@ -20,4 +21,9 @@ $(document).ready(function(){
$('nav').removeClass('visible');
});
// Close alerts
$('.alert-dismissible .close').click(function() {
$(this).parent().slideUp(500);
});
});

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

107
test.js
View File

@ -1,20 +1,11 @@
var chai = require('chai'),
const chai = require('chai'),
chaiHttp = require('chai-http'),
request = require('supertest'),
server = require('./server'),
should = chai.should(),
expect = chai.expect();
server = require('./server');
chai.use(chaiHttp);
describe('Index', function() {
// I think this restarts the server after each try?
// var server;
// beforeEach(function() {
// server = require('./server');
// });
// afterEach(function() {
// server.close();
// });
describe('Pages', 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)
@ -40,50 +49,9 @@ describe('Index', function() {
describe('Auth', 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){
@ -106,9 +74,9 @@ describe('Auth', function() {
// });
});
// });
describe('Map controls', function() {
// describe('Map controls', function() {
//TODO: it('Sets location', function(done){
@ -126,24 +94,5 @@ describe('Map controls', function() {
// });
});
// });
describe('Map popups', function() {
//TODO: it('Opens Share popup', function(done){
// });
//TODO: it('Closes Share popup', function(done){
// });
//TODO: it('Opens Settings popup', function(done){
// });
//TODO: it('Closes Settings popup', function(done){
// });
});

View File

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

View File

@ -1,14 +1,12 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | {{ code }} Error{% endblock %}
{% block title %}{{super()}} | 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 %}
<p>I would really appreciate it if you would <a href="https://github.com/Tracman-org/Server/issues/new">report this error</a>. </p>
{% if code %}<img style="width:100%" src="https://http.cat/{{code}}.jpg">{% endif %}
</div>
<section class='container'>
{% if code %}<h2>{{code}}</h2>{% endif %}
{% if message %}<h3>{{message}}</h3>{% endif %}
{% if stack %}<p>{{stack}}</p>{% endif %}
{% if not stack %}<p>I would really appreciate it if you would <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 %}
</section>
{% endblock %}

26
views/forgot.html Normal file
View File

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

View File

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

61
views/login.html Normal file
View File

@ -0,0 +1,61 @@
{% 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">
<style>
p, input, #social-login {
margin-bottom: 5vh;
}
</style>
{% 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' style="justify-content:space-around">
<a href="/login/google" class='gp btn'><i class="fa fa-google-plus"></i></a>
<a href="/login/facebook" class='fb btn'><i class="fa fa-facebook"></i></a>
<a href="/login/twitter" class='tw btn'><i class="fa fa-twitter"></i></a>
</div>
<div class='form-group' style="flex-wrap:wrap">
<input type="email" placeholder="Email" name="email" required>
<input type="password" placeholder="Password" name="password" required>
</div>
<input type="submit" value="Sign in" class='btn main'>
<p><a href="/login/forgot">Forgot your password?</a></p>
</form>
</div>
<hr class='hide'>
<div 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 %}

View File

@ -1,9 +1,9 @@
{% 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">
<link href="/static/css/.map.min.css" rel="stylesheet">
<style>
.wrap { top:{% if not noHeader %}58{% else %}0{% endif %}px;}
@ -147,6 +147,7 @@
// Google maps API callback
window.gmapsCb = function() {
// Create map
if (disp!='1'||!settings.showStreetview) {
map = new google.maps.Map( mapElem, {
@ -160,7 +161,7 @@
});
marker = new google.maps.Marker({
position: { lat:last.lat, lng:last.lon },
title: {{ mapuser.name | dump | safe }},
{% if mapuser.name.length %}title: {{mapuser.name|dump|safe}},{% endif %}
map: map,
draggable: false
});
@ -341,19 +342,6 @@
}
}
// 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

36
views/password.html Normal file
View File

@ -0,0 +1,36 @@
{% 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 input {
min-width: 40%;
}
</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 secure hash on the server. </p>
<div id='password' class='form-group' title="Type your new password here">
<input class='form-control' name="password" type="password" placeholder="enter password" minlength="8" maxlength="160">
<input class='form-control' name="repassword" type="password" placeholder="retype password" minlength="8" maxlength="160">
</div>
<div id='submit-group' class='form-group flexbox' style="padding:0 0 60px; justify-content:space-around">
<input class='btn yellow' style="width:50%; background:#333" type="submit" value="Save">
<a href="#" class='btn'>cancel</a>
</div>
</form>
</section>
{% endblock %}

22
views/privacy.html Normal file
View File

@ -0,0 +1,22 @@
{% extends 'templates/base.html' %}
{% block title %}{{super()}} | Privacy Policy{% endblock %}
{% block main %}
<section class='container'>
<h2>Privacy Policy</h2>
<p>In lieu of legalease, which I don't speak, here is a quick rundown of what Tracman does with your data (such as location). </p>
<h3 id='location-history'>Location history</h3>
<p>Your location is saved on the database as long as you have it "set" or "tracking". If you "clear" the data, it will be deleted from the database too. This doesn't mean all copies are destroyed. Our servers keep occasional backups, and caches could exist on other servers (google index, wayback archive, etc). </p>
<p>This means that all public access to your location is essentially deleted when you clear it. But anyone could record your location while it's publicly available and rebroadcast it. Tracman doesn't store location histories (except as mentioned above), but histories may exist elsewhere! If you have (or plan to have) trouble with the law, don't use Tracman. Authorities have easy access to those histories. </p>
<h3 id='email'>Email addresses</h3>
<p>Tracman stores email addresses so we can contact users for important stuff (urgent security updates, deletion requests, lost passwords). We will never subscribe you to anything else by default. </p>
</section>
{% endblock %}

View File

@ -1,134 +1,218 @@
{% extends 'templates/base.html' %}
{% block title %}{{super()}} | Settings{% endblock %}
{% block head %}
{{super()}}
<link rel="stylesheet" type="text/css" href="/static/css/.form.min.css">
{% endblock %}
{% block main %}
<section class='container'>
<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>
<h1>Settings</h1>
<form id='settings-form' role="form" method="post">
<h2>Account settings</h2>
<div id='name' class='form-group' title="This appears in your page's title. ">
<label for="name">Name</label>
<input class='form-control' name="name" type="text" value="{{user.name}}" maxlength="160">
</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>
<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>
<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 id='email' class='form-group' title="For account stuff, no dumb newsletters. ">
<label for="email">Email</label>
<input class='form-control' name="email" type="email" value="{{user.email}}" maxlength="160">
</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>
</div>
<div id='social-connect' class='form-group'>
<style>
<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>
#social-connect {
flex-wrap: wrap;
}
#social-connect > .btn {
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);
}
</style>
<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>
<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='password-delete' class='form-group'>
<a 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 class='red underline' style="text-align:right" href="#" onclick="deleteAccount()" title="Permently delete your Tracman account. ">Delete account</a>
</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>
<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='input-addon' size="13" value="tracman.org/map/" disabled readonly>
<input type="text" class='input-with-addon' name="slug" value="{{user.slug}}" maxlength="160" required>
</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='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='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='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='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">
<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 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' style="padding:0 0 60px; justify-content:space-around">
<input class='btn yellow' style="width:50%; background:#333" type="submit" value="Save">
<a href="#" class='btn' style="width:50%; background:#333">cancel</a>
<a href="#" class='btn'>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 %}
{% block javascript %}
{{super()}}
<script>
// Delete account
function deleteAccount() {
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!");
}
})
}
}
</script>
{% endblock %}

View File

@ -1,14 +1,30 @@
<!doctype html>
<!--
Tracman: GPS tracking service in node.js
Copyright © 2017 Keith Irwin
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
(A full copy of this programs license is available at https://github.com/Tracman-org/Server/blob/master/LICENSE.md)
-->
<html>
<head>
{% block head %}
<title>{% block title %}Tracman{% endblock %}</title>
<link rel="manifest" href="/static/manifest.webmanifest">
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<meta http-equiv="Content-type" content="text/html;charset=utf-8">
<meta charset="UTF-8">
<meta name="author" content="Keith Irwin">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
<meta name="keywords" content="map, phone, gps, link, location, track, friends, app">
<meta name="description" content="Tracman lets you see and share your phone's exact realtime location">
<meta name="theme-color" content="#222">
@ -25,26 +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>
{% 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>
{% if not noHeader %}{% include 'templates/header.html' %}{% endif %}
{% block main %}Loading... {% endblock %}
{% if not noFooter %}{% include 'templates/footer.html' %}{% endif %}
<main>
{% block main %}{% endblock %}
{% if not noFooter %}{% include 'templates/footer.html' %}{% endif %}
</main>
<!-- Google Analytics -->
<!-- Javascript -->
{% block javascript %}
<script>
// Google analytics
(function(t,r,a,c,m,o,n){t['GoogleAnalyticsObject']=m;t[m]=t[m]||function(){
(t[m].q=t[m].q||[]).push(arguments);},t[m].l=1*new Date();o=r.createElement(a),
n=r.getElementsByTagName(a)[0];o.async=1;o.src=c;n.parentNode.insertBefore(o,n);
@ -53,19 +72,9 @@
ga('create','UA-44266909-3','auto');
ga('require','linkid');
ga('send','pageview');
</script>
<!-- Firebase -->
<script src="https://www.gstatic.com/firebasejs/3.7.0/firebase.js"></script>
<script>
firebase.initializeApp({
apiKey: "AIzaSyDPYY_Fw3FXLm0hKfIfc8qlrc98zZiN4IY",
authDomain: "tracman-b894f.firebaseapp.com",
databaseURL: "https://tracman-b894f.firebaseio.com",
storageBucket: "tracman-b894f.appspot.com",
messagingSenderId: "483494341936"
});
</script>
{% endblock %}
</body>
</html>

View File

@ -13,4 +13,6 @@
<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>
</div>
</footer>
</footer>
<script src="/static/js/.footer.min.js"></script>

View File

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