Fix merge conflicts
commit
5b16e19c86
|
@ -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
|
39
LICENSE.md
39
LICENSE.md
|
@ -200,42 +200,3 @@ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY C
|
|||
### 17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
|
||||
|
||||
## END OF TERMS AND CONDITIONS
|
||||
|
||||
#### How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
|
13
PRIVACY.md
13
PRIVACY.md
|
@ -1,13 +0,0 @@
|
|||
# Privacy Policy for Tracman 0.3.1
|
||||
|
||||
In lieu of legalease, which I don't speak, here is a quick rundown of what Tracman does with your data (such as location).
|
||||
|
||||
## Location history
|
||||
|
||||
Your location is saved on the database as long as you have it "set" or "tracking". If you "clear" the data, it will be deleted from the database too. This doesn't mean all copies are destroyed. Our servers keep occasional backups, and caches could exist on other servers (google index, wayback archive, etc).
|
||||
|
||||
This means that all public access to your location is essentially deleted when you clear it. But anyone could record your location while it's publicly available and rebroadcast it. Tracman doesn't store location histories (except as mentioned above), but histories may exist elsewhere! If you have (or plan to have) trouble with the law, don't use Tracman. Authorities have easy access to those histories.
|
||||
|
||||
## Email addresses
|
||||
|
||||
Tracman stores email addresses so we can contact users for important stuff (urgent security updates, deletion requests, lost passwords). We will never subscribe you to anything else by default.
|
66
README.md
66
README.md
|
@ -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/)>.
|
325
config/auth.js
325
config/auth.js
|
@ -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); }
|
||||
} );
|
||||
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
||||
|
||||
};
|
|
@ -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}>`;
|
||||
}
|
||||
|
||||
};
|
|
@ -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
|
||||
}
|
||||
|
||||
};
|
|
@ -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)
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
name: {type:String, required:true},
|
||||
email: String,
|
||||
slug: {type:String, required:true, unique:true},
|
||||
requestId: String,
|
||||
isAdmin: {type:Boolean, required:true, default:false},
|
||||
isPro: {type:Boolean, required:true, default:false},
|
||||
created: Date,
|
||||
lastLogin: Date,
|
||||
googleID: {type:Number, unique:true},
|
||||
settings: {
|
||||
units: {type:String, default:'standard'},
|
||||
defaultMap: {type:String, default:'road'},
|
||||
defaultZoom: {type:Number, default:11},
|
||||
showSpeed: {type:Boolean, default:false},
|
||||
showTemp: {type:Boolean, default:false},
|
||||
showAlt: {type:Boolean, default:false},
|
||||
showStreetview: {type:Boolean, default:false}
|
||||
},
|
||||
last: {
|
||||
time: Date,
|
||||
lat: {type:Number, default:0},
|
||||
lon: {type:Number, default:0},
|
||||
dir: {type:Number, default:0},
|
||||
alt: {type:Number, default:0},
|
||||
spd: {type:Number, default:0}
|
||||
},
|
||||
sk32: {type:String, required:true, unique:true}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('User', userSchema);
|
|
@ -0,0 +1,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;
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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]:'',
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"verbose": true,
|
||||
"ext": "html, js, json, css",
|
||||
"events": {
|
||||
"start": "npm run minify"
|
||||
}
|
||||
}
|
12
package.json
12
package.json
|
@ -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
143
server.js
|
@ -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;
|
||||
|
|
|
@ -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
|
@ -1,10 +1,13 @@
|
|||
footer {
|
||||
font-weight: 300;
|
||||
width:100%;
|
||||
overflow:auto;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
background: #111;
|
||||
color: #ccc;
|
||||
padding: 0 20px;
|
||||
-moz-box-shadow: inset 0 .25vw 1vw #222;
|
||||
-webkit-box-shadow: inset 0 .25vw 1vw #222;
|
||||
box-shadow: inset 0 .25vw 1vw #222;
|
||||
}
|
||||
footer .left {
|
||||
float: left;
|
||||
|
@ -33,7 +36,6 @@ footer a .fa {
|
|||
footer .fa a:hover, footer .fa a:focus {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
footer {
|
||||
padding: 0 10px;
|
||||
|
@ -49,4 +51,4 @@ footer .fa a:hover, footer .fa a:focus {
|
|||
footer .right {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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;
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
/* Main */
|
||||
header {
|
||||
background: #222;
|
||||
padding: 0;
|
||||
|
@ -5,9 +6,12 @@ header {
|
|||
top: 0; left: 0;
|
||||
width: 100%;
|
||||
z-index: 200;
|
||||
} header a:hover, header a:focus {
|
||||
color: #fbc93d;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
header .logo {
|
||||
|
||||
float: left;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
padding: 13px 23px;
|
||||
|
@ -16,53 +20,43 @@ header .logo {
|
|||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
margin: 0;
|
||||
}
|
||||
header a:hover, header a:focus {
|
||||
color: #fbc93d;
|
||||
}
|
||||
header .logo a {
|
||||
} header .logo a {
|
||||
color:inherit;
|
||||
font:inherit;
|
||||
text-decoration:inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
header .logo img {
|
||||
} header .logo img {
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
width:28px;
|
||||
height:28px;
|
||||
vertical-align: middle;
|
||||
} header .logo:hover {
|
||||
text-decoration: none;
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
header nav {
|
||||
float: right;
|
||||
}
|
||||
header nav ul {
|
||||
} header nav ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
header nav ul li {
|
||||
} header nav ul li {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
header nav ul li a, header nav ul li span {
|
||||
} header nav ul li a, header nav ul li span {
|
||||
text-decoration:inherit;
|
||||
display: inline-block;
|
||||
padding: 15px 20px;
|
||||
color: #fff;
|
||||
transition: 200ms;
|
||||
}
|
||||
header nav ul li a:hover,
|
||||
transition: 100ms;
|
||||
} header nav ul li a:hover,
|
||||
header nav ul li a:focus,
|
||||
header nav ul li a.active,
|
||||
header .logo:hover {
|
||||
text-decoration: none;
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
.alert.header {
|
||||
position: relative;
|
||||
border-radius: 0;
|
||||
top: 58px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Hamburger */
|
||||
header .hamburger {
|
||||
display: none;
|
||||
padding: 5px;
|
||||
|
@ -70,18 +64,20 @@ header .hamburger {
|
|||
transition-property: opacity, -webkit-filter;
|
||||
transition-property: opacity, filter;
|
||||
transition-property: opacity, filter, -webkit-filter;
|
||||
transition-duration: 0.15s;
|
||||
transition-timing-function: linear; }
|
||||
.hamburger:hover {
|
||||
opacity: 0.7; }
|
||||
header .hamburger-box {
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: linear;
|
||||
} header .hamburger:hover {
|
||||
opacity: 0.7;
|
||||
} header .hamburger-box {
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
position: relative; }
|
||||
header .hamburger-inner {
|
||||
position: relative;
|
||||
} header .hamburger-inner {
|
||||
top: 50%;
|
||||
margin-top: -2px; }
|
||||
header .hamburger-inner, header .hamburger-inner::before, header .hamburger-inner::after {
|
||||
margin-top: -2px;
|
||||
} header .hamburger-inner,
|
||||
header .hamburger-inner::before,
|
||||
header .hamburger-inner::after {
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
background-color: #fff;
|
||||
|
@ -90,42 +86,47 @@ header .hamburger-inner {
|
|||
transition-property: -webkit-transform;
|
||||
transition-property: transform;
|
||||
transition-property: transform, -webkit-transform;
|
||||
transition-duration: 0.15s;
|
||||
transition-timing-function: ease; }
|
||||
header .hamburger-inner::before, header .hamburger-inner::after {
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease;
|
||||
} header .hamburger-inner::before, header .hamburger-inner::after {
|
||||
content: "";
|
||||
display: block; }
|
||||
header .hamburger-inner::before {
|
||||
top: -10px; }
|
||||
header .hamburger-inner::after {
|
||||
bottom: -10px; }
|
||||
|
||||
header .hamburger--slider .hamburger-inner {
|
||||
top: 0; }
|
||||
header .hamburger--slider .hamburger-inner::before {
|
||||
display: block;
|
||||
} header .hamburger-inner::before {
|
||||
top: -10px;
|
||||
} header .hamburger-inner::after {
|
||||
bottom: -10px;
|
||||
} header .hamburger--slider .hamburger-inner {
|
||||
top: 0;
|
||||
} header .hamburger--slider .hamburger-inner::before {
|
||||
top: 10px;
|
||||
transition-property: opacity, -webkit-transform;
|
||||
transition-property: transform, opacity;
|
||||
transition-property: transform, opacity, -webkit-transform;
|
||||
transition-timing-function: ease;
|
||||
transition-duration: 0.2s; }
|
||||
header .hamburger--slider .hamburger-inner::after {
|
||||
top: 20px; }
|
||||
|
||||
header .hamburger--slider.is-active .hamburger-inner {
|
||||
-webkit-transform: translate3d(0, 10px, 0) rotate(45deg);
|
||||
transform: translate3d(0, 10px, 0) rotate(45deg); }
|
||||
header .hamburger--slider.is-active .hamburger-inner::before {
|
||||
transition-duration: 200ms;
|
||||
} header .hamburger--slider .hamburger-inner::after {
|
||||
top: 20px;
|
||||
} header .hamburger--slider.is-active .hamburger-inner {
|
||||
-webkit-transform: translate3d(0, 10px, 0) rotate(45deg);
|
||||
-moz-transform: translate3d(0, 10px, 0) rotate(45deg);
|
||||
-md-transform: translate3d(0, 10px, 0) rotate(45deg);
|
||||
-o-transform: translate3d(0, 10px, 0) rotate(45deg);
|
||||
transform: translate3d(0, 10px, 0) rotate(45deg);
|
||||
} header .hamburger--slider.is-active .hamburger-inner::before {
|
||||
-webkit-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
|
||||
transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
|
||||
opacity: 0; }
|
||||
header .hamburger--slider.is-active .hamburger-inner::after {
|
||||
-moz-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
|
||||
-ms-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
|
||||
-o-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
|
||||
transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
|
||||
opacity: 0;
|
||||
} header .hamburger--slider.is-active .hamburger-inner::after {
|
||||
-webkit-transform: translate3d(0, -20px, 0) rotate(-90deg);
|
||||
transform: translate3d(0, -20px, 0) rotate(-90deg); }
|
||||
|
||||
|
||||
-moz-transform: translate3d(0, -20px, 0) rotate(-90deg);
|
||||
-ms-transform: translate3d(0, -20px, 0) rotate(-90deg);
|
||||
-o-transform: translate3d(0, -20px, 0) rotate(-90deg);
|
||||
transform: translate3d(0, -20px, 0) rotate(-90deg);
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
header { padding:0; }
|
||||
header nav ul li a { padding:15px; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
|
@ -138,17 +139,14 @@ header .hamburger--slider.is-active .hamburger-inner {
|
|||
width: 100%;
|
||||
max-width: 300px;
|
||||
background: #333;
|
||||
transition: 200ms;
|
||||
}
|
||||
header nav.visible {
|
||||
transition: 100ms;
|
||||
} header nav.visible {
|
||||
right: 0px;
|
||||
}
|
||||
header nav ul li {
|
||||
} header nav ul li {
|
||||
display: block;
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
header nav ul li a {
|
||||
} header nav ul li a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
|
@ -161,3 +159,64 @@ header .hamburger--slider.is-active .hamburger-inner {
|
|||
top: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
noscript .alert-danger {
|
||||
z-index: 40;
|
||||
}
|
||||
.alert-danger {
|
||||
z-index: 30;
|
||||
color: #f2dede;
|
||||
background-color: #a94442;
|
||||
}
|
||||
.alert-warning {
|
||||
z-index: 20;
|
||||
color: #fcf8e3;
|
||||
background-color: #8a6d3b;
|
||||
}
|
||||
.alert-success {
|
||||
z-index: 10;
|
||||
color: #dff0d8;
|
||||
background-color: #3c763d;
|
||||
}
|
||||
.alert.alert-header {
|
||||
position: relative;
|
||||
border-radius: 0;
|
||||
top: 58px;
|
||||
width: 100%;
|
||||
}
|
||||
.alert a {
|
||||
z-index: 10;
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.alert a:hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.alert h4 {
|
||||
margin-top: 0;
|
||||
color: inherit;
|
||||
}
|
||||
.alert > p,
|
||||
.alert > ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.alert > p + p {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.alert-dismissable {
|
||||
padding-right: 35px;
|
||||
}
|
||||
.alert .close,
|
||||
.alert-dismissible .close {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
color: inherit;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
/* End Animations */
|
||||
|
||||
.btn { border-radius: 50px; }
|
||||
.container > p { margin-bottom: 5vh; }
|
||||
|
||||
.splash {
|
||||
background: #090909;
|
||||
|
@ -165,6 +166,15 @@
|
|||
.light h2 {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.light .btn {
|
||||
color: #111;
|
||||
background: rgba(0,0,0,0.1);
|
||||
}
|
||||
.light .btn:hover:not(.disabled) {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
color: #fb6e3d;
|
||||
|
|
|
@ -0,0 +1,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; }
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
'use strict';
|
||||
/* global $ */
|
||||
|
||||
// Push footer to bottom on pages with little content
|
||||
function setFooter(){
|
||||
var windowHeight = $(window).height(),
|
||||
footerBottom = $("footer").offset().top + $("footer").height();
|
||||
if (windowHeight > footerBottom){
|
||||
$("footer").css( "margin-top", windowHeight-footerBottom );
|
||||
}
|
||||
}
|
||||
|
||||
// Execute on page load
|
||||
$(function(){ setFooter(); });
|
||||
|
||||
// Execute on window resize
|
||||
$(window).resize(function(){ setFooter(); });
|
|
@ -1,3 +1,4 @@
|
|||
/* global $ */
|
||||
'use strict';
|
||||
|
||||
$(document).ready(function(){
|
||||
|
@ -20,4 +21,9 @@ $(document).ready(function(){
|
|||
$('nav').removeClass('visible');
|
||||
});
|
||||
|
||||
// Close alerts
|
||||
$('.alert-dismissible .close').click(function() {
|
||||
$(this).parent().slideUp(500);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
jQuery.extend(jQuery.easing,{
|
||||
easeInOutExpo: function(x, t, b, c, d){
|
||||
if (t==0) return b;
|
||||
if (t==d) return b+c;
|
||||
if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
|
||||
return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function(){
|
||||
|
||||
$('a[href=#]').click(function(e){
|
||||
e.preventDefault();
|
||||
$('nav').removeClass('visible');
|
||||
$('html,body').stop().animate({scrollTop: $('.'+$(this).data('scrollto')).offset().top-65 }, 700, 'easeInOutExpo', function(){});
|
||||
});
|
||||
|
||||
});
|
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
107
test.js
|
@ -1,20 +1,11 @@
|
|||
var chai = require('chai'),
|
||||
const chai = require('chai'),
|
||||
chaiHttp = require('chai-http'),
|
||||
request = require('supertest'),
|
||||
server = require('./server'),
|
||||
should = chai.should(),
|
||||
expect = chai.expect();
|
||||
server = require('./server');
|
||||
chai.use(chaiHttp);
|
||||
|
||||
describe('Index', function() {
|
||||
// I think this restarts the server after each try?
|
||||
// var server;
|
||||
// beforeEach(function() {
|
||||
// server = require('./server');
|
||||
// });
|
||||
// afterEach(function() {
|
||||
// server.close();
|
||||
// });
|
||||
|
||||
describe('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){
|
||||
|
||||
// });
|
||||
|
||||
});
|
||||
|
|
|
@ -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 */ {
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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. Then send your friends a link with a map showing your live location. </p>
|
||||
<p>Download the android app and log in. Then send your friends a link with a map showing your live location. </p>
|
||||
</div>
|
||||
<div>
|
||||
<i class='fa fa-bolt'></i>
|
||||
<h3>Realtime</h3>
|
||||
<p>Your location updates every second for all the world to see. </p>
|
||||
<p>Your location updates every second for all the world to see. </p>
|
||||
</div>
|
||||
<div>
|
||||
<i class='fa fa-usd'></i>
|
||||
<h3>Free</h3>
|
||||
<p>It's free, but you can <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=hypergeek14%40gmail%2ecom&lc=US&item_name=Tracman¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted">donate</a> if you want to help with server expenses. </p>
|
||||
<p>It's free, but you can <a href="https://cash.me/$KeithIrwin">donate</a> if you want to help with server expenses. </p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -51,22 +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. There's also has an android app which can run in the background. With the app, you can: </p>
|
||||
<p>You can track your GPS location from your phone's web browsers. There's also has an android app which can run in the background. With the app, you can: </p>
|
||||
<ul>
|
||||
<li>
|
||||
<i class='fa fa-toggle-on'></i>
|
||||
<h4>Turn off tracking</h4>
|
||||
<p>If you need to go undercover, just turn tracman off with the flip of a switch. </p>
|
||||
<p>If you need to go undercover, just turn tracman off with the flip of a switch. </p>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-cog'></i>
|
||||
<h4>Change settings</h4>
|
||||
<p>Change your settings to show a less accurate location, if you want an air of mystery. </p>
|
||||
<p>Change your settings to show a less accurate location, if you want an air of mystery. </p>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-battery-3'></i>
|
||||
<h4>Save energy</h4>
|
||||
<p>If nobody's tracking you, tracman won't needlessly drain your battery. </p>
|
||||
<p>If nobody's tracking you, tracman won't needlessly drain your battery. </p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -78,34 +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. It'll look <a href="/map/keith">like this</a>. </p>
|
||||
<p>You'll get a simple webpage with a map to send to friends. It'll look <a href="/map/keith">like this</a>. </p>
|
||||
<ul>
|
||||
<li>
|
||||
<i class='fa fa-hand-o-right'></i>
|
||||
<h4>Easy</h4>
|
||||
<p>Just send a link to whomever you want. Bam, now they know where you are. </p>
|
||||
<p>Just send a link to whomever you want. Bam, now they know where you are. </p>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-map-marker'></i>
|
||||
<h4>Precise</h4>
|
||||
<p>Map updates in realtime with websockets. </p>
|
||||
<p>Map updates in realtime with websockets. </p>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-cogs'></i>
|
||||
<h4>Customizable</h4>
|
||||
<p>Change the map default type and zoom level. You can also show speed, altitude, and streetview. </p>
|
||||
<p>Change the map default type and zoom level. You can also show speed, altitude, and streetview. </p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class='disclaimer light' id='disclaimer'>
|
||||
<section class='disclaimer' id='disclaimer'>
|
||||
<div class='container'>
|
||||
<h2>Warning! </h2>
|
||||
<p>This is beta software, so there are still kinks to be worked out. </p>
|
||||
<p>Also keep in mind that publishing your location online could be a bad idea. </p>
|
||||
<p>I assume no responsibilities. </p>
|
||||
<p>This is beta software, so there are still kinks to be worked out. </p>
|
||||
<p>Keep in mind that publishing your location online could be a bad idea. </p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -113,9 +109,8 @@
|
|||
<section class='join light' id='join'>
|
||||
<div class='container'>
|
||||
<h2>Hook me up!</h2>
|
||||
<h3>Right now, Tracman is in beta testing. Things may break. </h3>
|
||||
<p>You will need a google account to join tracman and log in. </p>
|
||||
<a class='btn btn-lg ' href="/login">Join Tracman</a>
|
||||
<p>Just click that there button to create an account. </p>
|
||||
<a class='btn btn-lg' href="/login#signup">Join Tracman</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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
|
||||
|
|
|
@ -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 %}
|
|
@ -0,0 +1,22 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
{% block title %}{{super()}} | Privacy Policy{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<section class='container'>
|
||||
|
||||
<h2>Privacy Policy</h2>
|
||||
|
||||
<p>In lieu of legalease, which I don't speak, here is a quick rundown of what Tracman does with your data (such as location). </p>
|
||||
|
||||
<h3 id='location-history'>Location history</h3>
|
||||
|
||||
<p>Your location is saved on the database as long as you have it "set" or "tracking". If you "clear" the data, it will be deleted from the database too. This doesn't mean all copies are destroyed. Our servers keep occasional backups, and caches could exist on other servers (google index, wayback archive, etc). </p>
|
||||
|
||||
<p>This means that all public access to your location is essentially deleted when you clear it. But anyone could record your location while it's publicly available and rebroadcast it. Tracman doesn't store location histories (except as mentioned above), but histories may exist elsewhere! If you have (or plan to have) trouble with the law, don't use Tracman. Authorities have easy access to those histories. </p>
|
||||
|
||||
<h3 id='email'>Email addresses</h3>
|
||||
|
||||
<p>Tracman stores email addresses so we can contact users for important stuff (urgent security updates, deletion requests, lost passwords). We will never subscribe you to anything else by default. </p>
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -1,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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue