master
Keith Irwin 2016-07-02 01:14:36 +00:00
parent 797beecb8f
commit 3a00dc8372
16 changed files with 111 additions and 349 deletions

View File

@ -1,5 +1,5 @@
# Tracman
###### v 0.3.0
###### v 0.4.0
node.js application to display a map with user's location.
@ -16,7 +16,14 @@ $ npm start
## Changelog
#### v0.4.0
* Opened registration
* Replaced 'Imperial' with 'Standard'
* Bug fixes
#### v0.3.0
* Unified map and dashboard UI
* Security updates
* Security updates
* New admin UI

View File

@ -11,60 +11,65 @@ passport.use(new GoogleStrategy({
callbackURL: secret.url+'/auth/google/callback',
passReqToCallback: true
}, function(req, accessToken, refreshToken, profile, done) {
// Check for user
User.findOne({googleID: profile.id}, function(err, user){
// error
if (err) { console.log('Error finding user with google ID: '+profile.id+'\n'+err); }
// User found
if (!err && user !== null) {
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 {
if (req.session.passport) /* create new user */ {
User.findById(req.session.passport.user, function(err,user){
if (err) {
console.log('Error finding invited user with passport session ID: '+req.session.passport.user+'\n'+err);
var failMessage = 'Something went wrong finding your session. Would you like to <a href="/bug">report this error</a>?'; }
else {
user.googleID = profile.id;
user.lastLogin = Date.now();
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 (invited) user '+err);
var failMessage = 'Something went wrong finding your session. 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>. ' }
done(null, user, { success:successMessage, failure:failMessage });
});
}
});
// Check for user
User.findOne({googleID: profile.id}, function(err, user){
// Error
if (err) { console.log('Error finding user with google ID: '+profile.id+'\n'+err); }
// User found
if (!err && user !== null) /* Log user in */ {
if (!user.name) { user.name=profile.displayName; }
user.lastLogin = Date.now();
user.save(function (err, raw) {
if (err) { throwErr(req,err); }
}); done(null, user);
}
// User not found
else /* create user */ {
var user, successMessage, failMessage, cbc=2;
user.googleID = profile.id;
user.email = profile.emails[0];
user.lastLogin = Date.now();
// Get slug
(function checkSlug(s,cb) {
//console.log('checking ',s);
User.findOne({slug:s}, function(err, existingUser){
if (err) { console.log('Slug check error for ',slug(request.name).toLowerCase(),+':',err); }
if (existingUser){
s = '';
while (s.length<6) {
s+='abcdefghijkmnpqrtuvwxy346789'.charAt(Math.floor(Math.random()*28));
}
checkSlug(s,cb);
} else { cb(s); }
});
})(slug(profile.name).toLowerCase(), function(newSlug){
user.slug = newSlug;
if (cbc>1) /* waiting on other calls */ { cbc--; }
else { done(null, user, { success:successMessage, failure:failMessage }); }
});
// Get 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 (invited) user '+err);
var failMessage = 'Something went wrong finding your session. 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 }); }
});
}
else /* user wasn't invited */ {
done(null,false, {error: 'User not found. Maybe you want to <a href="#" data-scrollto="get">request an invite</a>? '});
}
}
});
});
}
});
}));

View File

@ -14,16 +14,14 @@ var throwErr = function(req,err){
var ensureAuth = function(req,res,next){
if (req.isAuthenticated()) { return next(); }
else {
req.flash('last',req.path);
res.redirect('/login');
}
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;

View File

@ -11,7 +11,7 @@ module.exports = mongoose.model('User', {
lastLogin: Date,
googleID: {type:Number, unique:true},
settings: {
units: {type:String, default:'imperial'},
units: {type:String, default:'standard'},
defaultMap: {type:String, default:'road'},
defaultZoom: {type:Number, default:11},
showSpeed: {type:Boolean, default:false},

View File

@ -40,7 +40,6 @@ router.route('/')
});
router.route('/requests')
.all(mw.ensureAdmin, function(req,res,next){
if (err) {

View File

@ -26,8 +26,10 @@ router.route('/')
});
// Not logged in
} else {
// Show index
}
// Not logged in
else {
res.render('index.html', {
error: req.flash('error')[0],
success: req.flash('success')[0],
@ -35,34 +37,7 @@ router.route('/')
inviteError: req.flash('request-error')[0]
});
}
}).post(function(req,res){ // Create request
Request.findOne({email:req.body.email}, function(err, request) {
if (err){ mw.throwErr(req,err); }
if (request){ // Already requested with this email
req.flash('request-error', 'Invite already requested! ');
res.redirect('/#get');
} else { // Send new request
request = new Request({
name: req.body.name,
email: req.body.email,
beg: req.body.why,
requestedTime: Date.now()
}); request.save(function(err) {
if (err){ mw.throwErr(req,err); }
mail.mailgun.messages().send({
from: 'Tracman Requests <requests@tracman.org>',
to: 'Keith Irwin <tracman@keithirwin.us>',
subject: 'New Tracman Invite request',
html: '<p>'+req.body.name+' requested a Tracman invite. </p><p>'+req.body.why+'</p><p><a href="https://tracman.org/admin/requests">See all invites</a></p>',
text: '\n'+req.body.name+' requested a Tracman invite. \n\n'+req.body.why+'\n\nhttps://tracman.org/admin/requests'
}, function(err,body){
if (err){ mw.throwErr(req,err); }
else { req.flash('request-success', 'Invite requested! '); }
res.redirect('/#get');
});
});
}
});
});
module.exports = router;

View File

@ -1,72 +0,0 @@
var router = require('express').Router(),
mw = require('../middleware.js'),
slug = require('slug'),
User = require('../models/user.js'),
Request = require('../models/request.js');
router.get('/:invite', function(req,res,next){
function associateUser(request,user){
request.userId = user._id;
request.save(function(err, raw){
if (err){ mw.throwErr(req,err); }
});
req.logIn(user, function(err) {
if (err) { mw.throwErr(req,err); }
user.lastLogin = Date.now();
user.save(function(err, raw) {
if (err) { mw.throwErr(req,err); }
res.redirect('/login');
});
});
}
User.findOne({requestId:req.params.invite}, function(err, existingUser) { // User already accepted invite
if (err) { console.log('Could not find existing user: '+err); }
if (existingUser && existingUser.gooogleID) { res.redirect('/login'); }
else {
Request.findById(req.params.invite, function(err, request) { // Check for granted invite
if (err) { mw.throwErr(req,err); }
if (!request) { next(); }
else {
if (existingUser) { // associate existing user with google account
associateUser(request,existingUser);
} else { // create new user
(function checkSlug(s,cb) {
console.log('checking ',s);
User.findOne({slug:s}, function(err, existingUser){
if (err) { console.log('Slug check error for ',slug(request.name).toLowerCase(),+':',err); }
if (existingUser){
s = '';
while (s.length<6) {
s+='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random()*62));
}
checkSlug(s,cb);
} else { cb(s); }
});
})(slug(request.name).toLowerCase(), function(newSlug){
newUser = new User({ // Create new user
requestId: request._id,
email: '',
slug: newSlug,
name: request.name,
created: Date.now(),
settings: {
units: 'imperial',
showSpeed: false,
showTemp: false,
showAlt: false,
showStreetview: true
}
})
newUser.save(function(err) {
if (err) { mw.throwErr(req,err); }
associateUser(request,newUser);
});
});
}
}
});
}
});
});
module.exports = router;

View File

@ -1,6 +1,6 @@
{
"name": "tracman",
"version": "0.3.0",
"version": "0.4.0",
"description": "Tracks user's GPS location",
"main": "server.js",
"dependencies": {

View File

@ -64,7 +64,6 @@
require('./config/routes/misc.js')
);
app.use(['/map','/trac'], require('./config/routes/map.js'));
app.use('/invited', require('./config/routes/invite.js'));
app.use('/admin', require('./config/routes/admin.js'));
app.use('/static', express.static(__dirname+'/static'));
}

View File

@ -178,30 +178,30 @@
color:#fb6e3d;
}
.get {
.join {
background: #fbc93d;
}
.get input, .get textarea {
.join input, .join textarea {
color:#111;
}
.get .input {
.join .input {
width: 47%;
float: left;
}
.get .submit {
.join .submit {
width: 47%;
float:right;
}
.get .input:nth-of-type(odd) {
.join .input:nth-of-type(odd) {
margin-right: 6%;
}
.get .message {
.join .message {
display: block;
clear: both;
float: none;
padding-top: 10px;
}
.get .input input {
.join .input input {
display: inline-block;
float: left;
width: 100%;
@ -209,7 +209,7 @@
border: 0;
padding: 10px 15px;
}
.get .message textarea {
.join .message textarea {
display: block;
width: 100%;
height: 200px;
@ -218,19 +218,19 @@
padding: 10px 15px;
resize: vertical;
}
.get label {
.join label {
position: relative;
z-index: 10;
}
.get label.input span, .get label.message span {
.join label.input span, .join label.message span {
display: inline-block;
float: left;
}
.get .submit {
.join .submit {
text-align: center;
padding-top: 10px;
}
.get .submit .btn, .get .submit .alert {
.join .submit .btn, .join .submit .alert {
position:static;
float:right;
}
@ -289,18 +289,18 @@
width: 100%;
float: none;
}
.get .input {
.join .input {
display: block;
width: 100%;
float: none;
}
.get .input:nth-of-type(odd) {
.join .input:nth-of-type(odd) {
margin-right: 0;
}
.get label {
.join label {
padding-top: 10px;
}
.get label:first-of-type {
.join label:first-of-type {
padding-top: 0;
}
}

View File

@ -1,5 +1,5 @@
{% extends 'templates/base.html' %}
{% block title %}Tracman | Invite Requests{% endblock %}
{% block title %}{{super()}} | Admin{% endblock %}
{% block head %}
{{ super() }}
@ -13,6 +13,7 @@
<section class='dark'>
<div class='container' id='tabs'>
<ul class='nav nav-tabs'>
<!--TODO: Why is bootstrap not formatting .nav-tabs -->
<li><a href="#users">Users</a></li>
<li><a href="#requests">Requests</a></li>
</ul>
@ -36,7 +37,7 @@
<td id='{{usr.id}}-created'></td>
<td id='{{usr.id}}-logged'></td>
<td id='{{usr.id}}-moved'></td>
<td id='{{usr.id}}-edit'><form action="" method="POST">
<td id='{{usr.id}}-edit'><form action="/users" method="POST">
<button type="submit" class='btn btn-block btn-danger' name="delete" value="{{usr.id}}">DELETE</button>
</form></td>
</tr>
@ -65,7 +66,7 @@
<td>{{ request.beg | replace("\r\n", "<br>") | safe }}</td>
<td id='{{request.id}}-requested'></td>
<td id='{{request.id}}-edit'>
<form action="" method="POST">
<form action="/requests" method="POST">
{% if not request.granted %}
<button type="submit" class='btn btn-block btn-success' name="invite" value="{{request.id}}">INVITE</button>
{% endif %}

View File

@ -1,69 +0,0 @@
{% extends 'templates/base.html' %}
{% block title %}Tracman | Invite Requests{% endblock %}
{% block head %}
{{ super() }}
<style>
.container { max-width:90%; }
</style>
{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
<h1>Requests</h1>
<table id='requests-table' class='table table-hover'>
<thead><tr>
<th>Name</th>
<th>Email</th>
<th>Message</th>
<th>Requested</th>
<th>Invited</th>
<th>User</th>
</tr></thead>
<tbody>
{% for request in requests %}
<tr class="table-{% if request.userId %}success{% elif request.granted %}info{% else %}danger{% endif %}">
<td>{{request.name}}</td>
<td>{{request.email}}</td>
<td>{{ request.beg | replace("\r\n", "<br>") | safe }}</td>
<td id='{{request.id}}-requested'></td>
<td id='{{request.id}}-edit'>
<form action="" method="POST">
{% if not request.granted %}
<button type="submit" class='btn btn-block btn-success' name="invite" value="{{request.id}}">INVITE</button>
{% endif %}
<button type="submit" class='btn btn-block btn-danger' name="delete" value="{{request.id}}">DELETE</button>
</form>
</td>
<td>
{% if request.userId %}
<a href="/map/id/{{request.userId}}">{{request.userId}}</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<script src="/static/js/moment.min.js"></script>
<script>
{% for request in requests %}
$('#{{request.id}}-requested').text(
moment("{{request.requestedTime}}", "ddd MMM DD YYYY HH:mm:ss [GMT]ZZ").fromNow()
// Sun Mar 20 2016 19:21:55 GMT+0100 (CET)
);
{% if request.granted %}
$('#{{request.id}}-edit').text(
moment("{{request.granted}}", "ddd MMM DD YYYY HH:mm:ss [GMT]ZZ").fromNow()
);
{% endif %}
{% endfor %}
</script>
{% endblock %}

View File

@ -1,69 +0,0 @@
{% extends 'templates/base.html' %}
{% block title %}Tracman | Users{% endblock %}
{% block head %}
{{ super() }}
<style>
.container { max-width:90%; }
</style>
{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
<h1>Users</h1>
<table id='users-table' class='table table-hover'>
<thead><tr>
<th>Name</th>
<th>Slug</th>
<th>Joined</th>
<th>Last login</th>
<th>Moved</th>
<th>Edit</th>
</tr></thead>
<tbody>
{% for usr in users %}
<tr>
<td>{{usr.name}}</td>
<td><a href="/map/{{usr.slug}}">/{{usr.slug}}</a></td>
<td id='{{usr.id}}-created'></td>
<td id='{{usr.id}}-logged'></td>
<td id='{{usr.id}}-moved'></td>
<td id='{{usr.id}}-edit'><form action="" method="POST">
<button type="submit" class='btn btn-block btn-danger' name="delete" value="{{usr.id}}">DELETE</button>
</form></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<script src="/static/js/moment.min.js"></script>
<script>
{% for usr in users %}
{% if usr.created %}
$('#{{usr.id}}-created').text(
moment("{{usr.created}}", "ddd MMM DD YYYY HH:mm:ss [GMT]ZZ").format('l')
);
{% endif %}
{% if usr.lastLogin %}
$('#{{usr.id}}-logged').text(
moment("{{usr.lastLogin}}", "ddd MMM DD YYYY HH:mm:ss [GMT]ZZ").fromNow()
);
{% endif %}
{% if usr.last.time %}
$('#{{usr.id}}-moved').text(
moment("{{usr.last.time}}", "ddd MMM DD YYYY HH:mm:ss [GMT]ZZ").fromNow()
);
{% else %}
$('#{{usr.id}}-moved').text("never");
{% endif %}
{% endfor %}
</script>
{% endblock %}

View File

@ -23,12 +23,12 @@
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div id='units' class='form-group col-xs-12' title="Select imperial units for feet and miles/hour. Select metric units if you are a commie. ">
<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="imperial" {% if user.settings.units == 'imperial' %}checked{% endif %}>
Imperial
<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 %}>

View File

@ -20,7 +20,7 @@
{% endif %}
{% else %}
<a class='btn' href="/map/keith">View example<i class='fa fa-angle-right'></i></a>
<a class='btn' href="#" data-scrollto="get">Request invite<i class='fa fa-angle-down'></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>
{% endif %}
</div>
@ -110,24 +110,12 @@
</section>
{% if not user %}
<section class='get light' id='get'>
<section class='join light' id='join'>
<div class='container'>
<h2>Hook me up!</h2>
<h3>Right now, Tracman is invite-only.&ensp;You can beg me for access here.&ensp;</h3>
<form id='invite-form' method="post">
<label class='input'><span>Name</span><input type="text" name="name" required></label>
<label class='input'><span>Email address</span><input type="email" name="email" required></label>
<label class='message'><span>Why you deserve beta access</span><textarea id='why' name="why"></textarea></label>
<label class='submit'>
{% if inviteSuccess.length > 0 %}
<div class='alert alert-success'><i class="fa fa-check-circle"></i> {{ inviteSuccess }}</div>
{% elif inviteError.length > 0 %}
<div class='alert alert-danger'><i class="fa fa-exclamation-circle"></i> {{ inviteError }}</div>
{% else %}
<button type="submit" class='btn'>Request Invite<i class='fa fa-angle-right'></i></button>
{% endif %}
</label>
</form>
<h3>Right now, Tracman is in beta testing.&ensp;Things may break.&ensp;</h3>
<p>You will need a google account to join tracman and log in.&ensp;</p>
<a class='btn btn-lg ' href="/login">Join Tracman</a>
</div>
</section>
{% endif %}

View File

@ -145,12 +145,12 @@
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div id='units' class='form-group col-xs-12' title="Select imperial units for feet and miles/hour. Select metric units if you are a commie. ">
<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="imperial" {% if user.settings.units == 'imperial' %}checked{% endif %}>
Imperial
<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 %}>
@ -316,7 +316,7 @@
// Parse location
function parseLoc(loc) {
loc.spd = (settings.units=='imperial')?parseFloat(loc.spd)*2.23694:parseFloat(loc.spd)
loc.spd = (settings.units=='standard')?parseFloat(loc.spd)*2.23694:parseFloat(loc.spd)
loc.dir = parseFloat(loc.dir);
loc.lat = parseFloat(loc.lat);
loc.lon = parseFloat(loc.lon);
@ -392,9 +392,9 @@
speedLabel.className = 'spd-label';
speedLabel.innerHTML = 'SPEED';
speedText.className = 'spd';
speedText.innerHTML = (settings.units=='imperial')?(parseFloat(last.spd)*2.23694).toFixed():last.spd.toFixed();
speedText.innerHTML = (settings.units=='standard')?(parseFloat(last.spd)*2.23694).toFixed():last.spd.toFixed();
speedUnit.className = 'spd-unit';
speedUnit.innerHTML = (settings.units=='imperial')?'m.p.h.':'k.p.h.';
speedUnit.innerHTML = (settings.units=='standard')?'m.p.h.':'k.p.h.';
speedSign.className = 'spd-sign';
speedSign.appendChild(speedLabel);
speedSign.appendChild(speedText);
@ -416,9 +416,9 @@
altitudeText.innerHTML = '';
altitudeLabel.innerHTML = 'ALTITUDE';
getAltitude(new google.maps.LatLng(last.lat,last.lon), elevator, function(alt) {
if (alt) { altitudeText.innerHTML = (settings.units=='imperial')?(alt*3.28084).toFixed():alt.toFixed(); }
if (alt) { altitudeText.innerHTML = (settings.units=='standard')?(alt*3.28084).toFixed():alt.toFixed(); }
});
altitudeUnit.innerHTML = (settings.units=='imperial')?'feet above sea level':'meters above sea level';
altitudeUnit.innerHTML = (settings.units=='standard')?'feet above sea level':'meters above sea level';
altitudeSign.appendChild(altitudeLabel);
altitudeSign.appendChild(altitudeText);
altitudeSign.appendChild(altitudeUnit);
@ -453,7 +453,7 @@
if (settings.showSpeed) { $('.spd').text(loc.spd.toFixed()); }
if (settings.showAlt) {
getAltitude({lat:loc.lat,lng:loc.lon}, elevator, function(alt) {
if (alt) { $('.alt').text((settings.units=='imperial')?(alt*3.28084).toFixed():alt.toFixed()); }
if (alt) { $('.alt').text((settings.units=='standard')?(alt*3.28084).toFixed():alt.toFixed()); }
});
}
toggleMaps(loc);