Merged release-0.9.0 into master
commit
10d0fe986e
|
@ -1,9 +1,13 @@
|
||||||
# npm
|
# npm
|
||||||
node_modules/
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
|
||||||
|
# Istanbul reports
|
||||||
|
coverage/
|
||||||
|
|
||||||
# Secret stuff
|
# Secret stuff
|
||||||
config/env/*
|
config/env/*
|
||||||
!config/env/sample.js
|
!config/env/sample.js
|
||||||
|
@ -16,39 +20,3 @@ static/**/*.bun.*
|
||||||
# Ignore docs files
|
# Ignore docs files
|
||||||
_gh_pages
|
_gh_pages
|
||||||
_site
|
_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
|
|
||||||
|
|
|
@ -5,4 +5,9 @@ branches:
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
build:
|
build:
|
||||||
echo "module.exports = require('./travis.js');" > config/env/env.js
|
- echo "module.exports = require('./travis.js')" > config/env/env.js
|
||||||
|
script:
|
||||||
|
- npm run cover
|
||||||
|
|
||||||
|
# Send coverage data to Coveralls
|
||||||
|
after_script: "cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js"
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -1,5 +1,13 @@
|
||||||
# Tracman Server Changelog
|
# Tracman Server Changelog
|
||||||
### v0.8.2
|
### v0.9.0
|
||||||
|
|
||||||
|
###### v0.9.0
|
||||||
|
* [#121](https://github.com/Tracman-org/Server/issues/121) Fixed various security holes
|
||||||
|
* [#68](https://github.com/Tracman-org/Server/issues/68) Added tests, mostly for authentication
|
||||||
|
* [#120](https://github.com/Tracman-org/Server/issues/120) Split config/routes/settings.js into two files
|
||||||
|
* Removed express validator and replaced with homegrown function
|
||||||
|
* Fixed showing welcome message on every login
|
||||||
|
* Removed naked domains
|
||||||
|
|
||||||
###### v0.8.1/2
|
###### v0.8.1/2
|
||||||
* Hotfixed service worker bugs
|
* Hotfixed service worker bugs
|
||||||
|
|
23
README.md
23
README.md
|
@ -1,11 +1,12 @@
|
||||||
# <img align="left" src="/static/img/icon/by/48.png" alt="T" title="The Tracman Logo">Tracman
|
# <img align="left" src="/static/img/icon/by/48.png" alt="T" title="The Tracman Logo">Tracman
|
||||||
###### v 0.8.2
|
###### v 0.9.0
|
||||||
|
|
||||||
node.js application to display a sharable map with user's location.
|
node.js application to display a sharable map with user's location.
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/Tracman-org/Server.svg?branch=develop)](https://travis-ci.org/Tracman-org/Server)
|
[![Travis Build Status](https://travis-ci.org/Tracman-org/Server.svg?branch=develop)](https://travis-ci.org/Tracman-org/Server)
|
||||||
|
[![Coverage Status](https://coveralls.io/repos/github/Tracman-org/Server/badge.svg?branch=master)](https://coveralls.io/github/Tracman-org/Server?branch=master)
|
||||||
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
|
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
|
||||||
[![Waffle.io - Columns and their card count](https://badge.waffle.io/Tracman-org/Server.svg?columns=all)](https://waffle.io/Tracman-org/Server)
|
[![Snyk Vulnerabilities](https://snyk.io/test/github/Tracman-org/Server/badge.svg)](https://snyk.io/test/github/Tracman-org/Servr)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ A good method is to simply copy the sample configuration and point `config/env/e
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cp config/env/sample.js config/env/local-config.js
|
cp config/env/sample.js config/env/local-config.js
|
||||||
echo "module.exports = require('./local-config.js');" > config/env/env.js
|
echo "module.exports = require('./local-config.js')" > config/env/env.js
|
||||||
```
|
```
|
||||||
|
|
||||||
Then edit `config/env/local-config.js` to match your local environment.
|
Then edit `config/env/local-config.js` to match your local environment.
|
||||||
|
@ -55,10 +56,16 @@ Tracman will be updated according to [this branching model](http://nvie.com/post
|
||||||
|
|
||||||
[view full changelog](CHANGELOG.md)
|
[view full changelog](CHANGELOG.md)
|
||||||
|
|
||||||
###### v0.8.1/2
|
###### v0.9.0
|
||||||
* Hotfixed service worker bugs
|
* [#121](https://github.com/Tracman-org/Server/issues/121) Fixed various security holes
|
||||||
|
* [#68](https://github.com/Tracman-org/Server/issues/68) Added tests, mostly for authentication
|
||||||
|
* [#120](https://github.com/Tracman-org/Server/issues/120) Split config/routes/settings.js into two files
|
||||||
|
* Removed express validator and replaced with homegrown function
|
||||||
|
* Fixed showing welcome message on every login
|
||||||
|
* Removed naked domains
|
||||||
|
|
||||||
#### v0.8.0
|
###### v0.8.x
|
||||||
|
* Hotfixed service worker bugs
|
||||||
* Added check to ensure only the newest location is sent
|
* Added check to ensure only the newest location is sent
|
||||||
* Removed buggy login/-out redirects
|
* Removed buggy login/-out redirects
|
||||||
* [#111](https://github.com/Tracman-org/Server/issues/111) Implemented service worker
|
* [#111](https://github.com/Tracman-org/Server/issues/111) Implemented service worker
|
||||||
|
@ -93,7 +100,7 @@ Tracman will be updated according to [this branching model](http://nvie.com/post
|
||||||
[view full license](LICENSE.md)
|
[view full license](LICENSE.md)
|
||||||
|
|
||||||
Tracman: GPS tracking service in node.js
|
Tracman: GPS tracking service in node.js
|
||||||
Copyright © 2017 [Keith Irwin](https://keithirwin.us/)
|
Copyright © 2018 [Keith Irwin](https://www.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 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.
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ const userSchema = new mongoose.Schema({
|
||||||
isPro: {type: Boolean, required: true, default: false},
|
isPro: {type: Boolean, required: true, default: false},
|
||||||
created: {type: Date, required: true},
|
created: {type: Date, required: true},
|
||||||
lastLogin: Date,
|
lastLogin: Date,
|
||||||
|
isNewUser: Boolean,
|
||||||
settings: {
|
settings: {
|
||||||
units: {type: String, default: 'standard'},
|
units: {type: String, default: 'standard'},
|
||||||
defaultMap: {type: String, default: 'road'},
|
defaultMap: {type: String, default: 'road'},
|
||||||
|
@ -78,14 +79,10 @@ userSchema.methods.createPassToken = function () {
|
||||||
|
|
||||||
return new Promise( async (resolve, reject) => {
|
return new Promise( async (resolve, reject) => {
|
||||||
|
|
||||||
// Reuse old token, resetting clock
|
// Reuse old token
|
||||||
if (user.auth.passTokenExpires >= Date.now()) {
|
if (user.auth.passTokenExpires >= Date.now()) {
|
||||||
debug(`Reusing old password token...`)
|
debug(`Reusing old password token...`)
|
||||||
user.auth.passTokenExpires = Date.now() + 3600000 // 1 hour
|
resolve([user.auth.passToken, user.auth.passTokenExpires])
|
||||||
try {
|
|
||||||
await user.save()
|
|
||||||
resolve([user.auth.passToken, user.auth.passTokenExpires])
|
|
||||||
} catch (err) { reject(err) }
|
|
||||||
|
|
||||||
// Create new token
|
// Create new token
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -7,6 +7,7 @@ const TwitterStrategy = require('passport-twitter').Strategy
|
||||||
const GoogleTokenStrategy = require('passport-google-id-token')
|
const GoogleTokenStrategy = require('passport-google-id-token')
|
||||||
const FacebookTokenStrategy = require('passport-facebook-token')
|
const FacebookTokenStrategy = require('passport-facebook-token')
|
||||||
const TwitterTokenStrategy = require('passport-twitter-token')
|
const TwitterTokenStrategy = require('passport-twitter-token')
|
||||||
|
const sanitize = require('mongo-sanitize')
|
||||||
const debug = require('debug')('tracman-passport')
|
const debug = require('debug')('tracman-passport')
|
||||||
const env = require('./env/env.js')
|
const env = require('./env/env.js')
|
||||||
const mw = require('./middleware.js')
|
const mw = require('./middleware.js')
|
||||||
|
@ -33,7 +34,7 @@ module.exports = (passport) => {
|
||||||
}, async (req, email, password, done) => {
|
}, async (req, email, password, done) => {
|
||||||
debug(`Perfoming local login for ${email}`)
|
debug(`Perfoming local login for ${email}`)
|
||||||
try {
|
try {
|
||||||
let user = await User.findOne({'email': email})
|
let user = await User.findOne({'email': sanitize(email)})
|
||||||
|
|
||||||
// No user with that email
|
// No user with that email
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -45,16 +46,16 @@ module.exports = (passport) => {
|
||||||
debug(`User exists. Checking password...`)
|
debug(`User exists. Checking password...`)
|
||||||
|
|
||||||
// Check password
|
// Check password
|
||||||
let res = await user.validPassword(password)
|
let correct_password = await user.validPassword(password)
|
||||||
|
|
||||||
// Password incorrect
|
// Password incorrect
|
||||||
if (!res) {
|
if (!correct_password) {
|
||||||
debug(`Incorrect password`)
|
debug(`Incorrect password`)
|
||||||
return done(null, false, req.flash('warning', 'Incorrect email or password.'))
|
return done(null, false, req.flash('warning', 'Incorrect email or password.'))
|
||||||
|
|
||||||
// Successful login
|
// Successful login
|
||||||
} else {
|
} else {
|
||||||
if (!user.lastLogin) req.forNewUser = true
|
user.isNewUser = !Boolean(user.lastLogin)
|
||||||
user.lastLogin = Date.now()
|
user.lastLogin = Date.now()
|
||||||
user.save()
|
user.save()
|
||||||
return done(null, user)
|
return done(null, user)
|
||||||
|
@ -143,11 +144,11 @@ module.exports = (passport) => {
|
||||||
// Check for unique profileId
|
// Check for unique profileId
|
||||||
debug(`Checking for unique account with query ${query}...`)
|
debug(`Checking for unique account with query ${query}...`)
|
||||||
try {
|
try {
|
||||||
let user = await User.findOne(query)
|
let existing_user = await User.findOne(query)
|
||||||
|
|
||||||
// Social account already in use
|
// Social account already in use
|
||||||
if (existingUser) {
|
if (existing_user) {
|
||||||
debug(`${service} account already in use with user ${existingUser.id}`)
|
debug(`${service} account already in use with user ${existing_user.id}`)
|
||||||
req.session.flashType = 'warning'
|
req.session.flashType = 'warning'
|
||||||
req.session.flashMessage = `Another user is already connected to that ${service} account. `
|
req.session.flashMessage = `Another user is already connected to that ${service} account. `
|
||||||
return done()
|
return done()
|
||||||
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const mw = require('../middleware.js')
|
||||||
|
const sanitize = require('mongo-sanitize')
|
||||||
|
const User = require('../models.js').user
|
||||||
|
const mail = require('../mail.js')
|
||||||
|
const env = require('../env/env.js')
|
||||||
|
const zxcvbn = require('zxcvbn')
|
||||||
|
const moment = require('moment')
|
||||||
|
const debug = require('debug')('tracman-routes-account')
|
||||||
|
const router = require('express').Router()
|
||||||
|
|
||||||
|
|
||||||
|
// Confirm email address
|
||||||
|
router.get('/email/:token', mw.ensureAuth, async (req, res, next) => {
|
||||||
|
// Check token
|
||||||
|
if (req.user.emailToken === req.params.token) {
|
||||||
|
try {
|
||||||
|
// Set new email
|
||||||
|
req.user.email = req.user.newEmail
|
||||||
|
|
||||||
|
// Delete token and newEmail
|
||||||
|
req.user.emailToken = undefined
|
||||||
|
req.user.newEmail = undefined
|
||||||
|
|
||||||
|
await req.user.save()
|
||||||
|
|
||||||
|
// Report success
|
||||||
|
req.flash('success', `Your email has been set to <u>${req.user.email}</u>. `)
|
||||||
|
res.redirect('/settings')
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
mw.throwErr(err, req)
|
||||||
|
res.redirect('/settings')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid token
|
||||||
|
} else {
|
||||||
|
req.flash('danger', 'Email confirmation token is invalid. ')
|
||||||
|
res.redirect('/settings')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set password
|
||||||
|
router.route('/password')
|
||||||
|
.all(mw.ensureAuth, (req, res, next) => {
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Email user a token, proceed at /password/:token
|
||||||
|
.get( async (req, res, next) => {
|
||||||
|
// Create token for password change
|
||||||
|
try {
|
||||||
|
let [token, expires] = await req.user.createPassToken()
|
||||||
|
// Figure out expiration time
|
||||||
|
let expirationTimeString = (req.query.tz)
|
||||||
|
? moment(expires).utcOffset(req.query.tz).toDate().toLocaleTimeString(req.acceptsLanguages[0])
|
||||||
|
: moment(expires).toDate().toLocaleTimeString(req.acceptsLanguages[0]) + ' UTC'
|
||||||
|
|
||||||
|
// Alert user to check email.
|
||||||
|
req.flash('success',
|
||||||
|
`An link has been sent to <u>${req.user.email}</u>. \
|
||||||
|
Click on the link to complete your password change. \
|
||||||
|
This link will expire in one hour (${expirationTimeString}). `
|
||||||
|
)
|
||||||
|
|
||||||
|
// Confirm password change request by email.
|
||||||
|
return await mail.send({
|
||||||
|
to: mail.to(req.user),
|
||||||
|
from: mail.noReply,
|
||||||
|
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}/account/password/${token}. \n\n\
|
||||||
|
This request will expire at ${expirationTimeString}. `
|
||||||
|
),
|
||||||
|
html: mail.html(
|
||||||
|
`<p>A request has been made to change your tracman password. \
|
||||||
|
If you did not initiate this request, please contact support at \
|
||||||
|
<a href="mailto:keith@tracman.org">keith@tracman.org</a>. </p>\
|
||||||
|
<p>To change your password, follow this link:\
|
||||||
|
<br><a href="${env.url}/account/password/${token}">\
|
||||||
|
${env.url}/account/password/${token}</a>. </p>\
|
||||||
|
<p>This request will expire at ${expirationTimeString}. </p>`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
mw.throwErr(err, req)
|
||||||
|
} finally {
|
||||||
|
res.redirect((req.user) ? '/settings' : '/login')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.route('/password/:token')
|
||||||
|
|
||||||
|
// Check token
|
||||||
|
.all( async (req, res, next) => {
|
||||||
|
debug('/account/password/:token .all() called')
|
||||||
|
try {
|
||||||
|
let user = await User
|
||||||
|
.findOne({'auth.passToken': sanitize(req.params.token)})
|
||||||
|
.where('auth.passTokenExpires').gt(Date.now())
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
debug('Bad token')
|
||||||
|
req.flash('danger', 'Password reset token is invalid or has expired. ')
|
||||||
|
res.redirect((req.isAuthenticated) ? '/settings' : '/login')
|
||||||
|
} else {
|
||||||
|
debug('setting passwordUser')
|
||||||
|
res.locals.passwordUser = user
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
mw.throwErr(err, req)
|
||||||
|
res.redirect('/password')
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Show password change form
|
||||||
|
.get((req, res) => {
|
||||||
|
debug('/account/password/:token .get() called')
|
||||||
|
res.render('password')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set new password
|
||||||
|
.post( async (req, res, next) => {
|
||||||
|
debug('/account/password/:token .post() called')
|
||||||
|
|
||||||
|
// Validate password strength
|
||||||
|
let zxcvbnResult = zxcvbn(req.body.password)
|
||||||
|
if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 864000) { // Less than ten days
|
||||||
|
req.flash( 'danger',
|
||||||
|
`That password could be cracked in ${zxcvbnResult.crack_times_display.online_no_throttling_10_per_second}! Come up with a more complex password that would take at least 10 days to crack. `
|
||||||
|
)
|
||||||
|
res.redirect(`/account/password/${req.params.token}`)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Create hashed password and save to db
|
||||||
|
try {
|
||||||
|
await res.locals.passwordUser.generateHashedPassword(req.body.password)
|
||||||
|
|
||||||
|
// User changed password
|
||||||
|
if (req.user) {
|
||||||
|
debug('User saved password')
|
||||||
|
req.flash('success', 'Your password has been changed. ')
|
||||||
|
res.redirect('/settings')
|
||||||
|
|
||||||
|
// New user created password
|
||||||
|
} else {
|
||||||
|
debug('New user created password')
|
||||||
|
req.flash('success', 'Password set. You can use it to log in now. ')
|
||||||
|
res.redirect('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
debug('Error creating hashed password and saving to db')
|
||||||
|
mw.throwErr(err, req)
|
||||||
|
res.redirect(`/account/password/${req.params.token}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
|
@ -6,6 +6,7 @@ const User = require('../models.js').user
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const moment = require('moment')
|
const moment = require('moment')
|
||||||
const slugify = require('slug')
|
const slugify = require('slug')
|
||||||
|
const sanitize = require('mongo-sanitize')
|
||||||
const debug = require('debug')('tracman-routes-auth')
|
const debug = require('debug')('tracman-routes-auth')
|
||||||
const env = require('../env/env.js')
|
const env = require('../env/env.js')
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ module.exports = (app, passport) => {
|
||||||
req.flash(req.session.flashType, req.session.flashMessage)
|
req.flash(req.session.flashType, req.session.flashMessage)
|
||||||
req.session.flashType = undefined
|
req.session.flashType = undefined
|
||||||
req.session.flashMessage = undefined
|
req.session.flashMessage = undefined
|
||||||
res.redirect('/map'+(req.forNewUser)?'/map?new=1':'')
|
res.redirect('/map')
|
||||||
}
|
}
|
||||||
const appLoginCallback = (req, res, next) => {
|
const appLoginCallback = (req, res, next) => {
|
||||||
debug('appLoginCallback called.')
|
debug('appLoginCallback called.')
|
||||||
|
@ -57,7 +58,7 @@ module.exports = (app, passport) => {
|
||||||
.post( async (req, res, next) => {
|
.post( async (req, res, next) => {
|
||||||
|
|
||||||
// Send token and alert user
|
// Send token and alert user
|
||||||
async function sendToken(user) {
|
const sendToken = async function(user) {
|
||||||
debug(`sendToken() called for user ${user.id}`)
|
debug(`sendToken() called for user ${user.id}`)
|
||||||
|
|
||||||
// Create a new password token
|
// Create a new password token
|
||||||
|
@ -80,14 +81,14 @@ module.exports = (app, passport) => {
|
||||||
subject: 'Complete your Tracman registration',
|
subject: 'Complete your Tracman registration',
|
||||||
text: mail.text(
|
text: mail.text(
|
||||||
`Welcome to Tracman! \n\nTo complete your registration, follow \
|
`Welcome to Tracman! \n\nTo complete your registration, follow \
|
||||||
this link and set your password:\n${env.url}/settings/password/${token}\n\n\
|
this link and set your password:\n${env.url}/account/password/${token}\n\n\
|
||||||
This link will expire at ${expiration_time_string}. `
|
This link will expire at ${expiration_time_string}. `
|
||||||
),
|
),
|
||||||
html: mail.html(
|
html: mail.html(
|
||||||
`<p>Welcome to Tracman! </p><p>To complete your registration, \
|
`<p>Welcome to Tracman! </p><p>To complete your registration, \
|
||||||
follow this link and set your password:\
|
follow this link and set your password:\
|
||||||
<br><a href="${env.url}/settings/password/${token}">\
|
<br><a href="${env.url}/account/password/${token}">\
|
||||||
${env.url}/settings/password/${token}</a></p>\
|
${env.url}/account/password/${token}</a></p>\
|
||||||
<p>This link will expire at ${expiration_time_string}. </p>`
|
<p>This link will expire at ${expiration_time_string}. </p>`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -131,114 +132,124 @@ module.exports = (app, passport) => {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate email
|
// Invalid email
|
||||||
req.checkBody('email', 'Please enter a valid email address.').isEmail()
|
if (!mw.validateEmail(req.body.email)) {
|
||||||
|
debug(`Email ${req.body.email} was found invalid!`)
|
||||||
// Check if somebody already has that email
|
req.flash('warning', `The email you entered, ${req.body.email} isn't valid. Try again. `)
|
||||||
try {
|
|
||||||
debug(`Searching for user with email ${req.body.email}...`)
|
|
||||||
let user = await User.findOne({'email': req.body.email})
|
|
||||||
|
|
||||||
// User already exists
|
|
||||||
if (user && user.auth.password) {
|
|
||||||
debug(`User ${user.id} has email ${req.body.email} and has a password`)
|
|
||||||
req.flash('warning',
|
|
||||||
`A user with that email already exists! If you forgot your password, \
|
|
||||||
you can <a href="/login/forgot?email=${req.body.email}">reset it here</a>.`
|
|
||||||
)
|
|
||||||
res.redirect('/login#login')
|
|
||||||
next()
|
|
||||||
|
|
||||||
// User exists but hasn't created a password yet
|
|
||||||
} else if (user) {
|
|
||||||
debug(`User ${user.id} has email ${req.body.email} but doesn't have a password`)
|
|
||||||
|
|
||||||
// Send another token
|
|
||||||
sendToken(user)
|
|
||||||
|
|
||||||
// Create user
|
|
||||||
} else {
|
|
||||||
debug(`User with email ${req.body.email} doesn't exist; creating one`)
|
|
||||||
|
|
||||||
let email = req.body.email
|
|
||||||
|
|
||||||
user = new User()
|
|
||||||
user.created = Date.now()
|
|
||||||
user.email = email
|
|
||||||
user.slug = slugify(email.substring(0, email.indexOf('@')))
|
|
||||||
|
|
||||||
// Generate unique slug
|
|
||||||
const slug = new Promise((resolve, reject) => {
|
|
||||||
debug(`Creating new slug for user...`);
|
|
||||||
|
|
||||||
(async function checkSlug (s, cb) {
|
|
||||||
try {
|
|
||||||
debug(`Checking to see if slug ${s} is taken...`)
|
|
||||||
let existingUser = await User.findOne({slug: s})
|
|
||||||
|
|
||||||
// Slug in use: generate a random one and retry
|
|
||||||
if (existingUser) {
|
|
||||||
debug(`Slug ${s} is taken; generating another...`)
|
|
||||||
crypto.randomBytes(6, (err, buf) => {
|
|
||||||
if (err) {
|
|
||||||
debug('Failed to create random bytes for slug!')
|
|
||||||
mw.throwErr(err, req)
|
|
||||||
reject()
|
|
||||||
}
|
|
||||||
if (buf) {
|
|
||||||
checkSlug(buf.toString('hex'), cb)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Unique slug: proceed
|
|
||||||
} else {
|
|
||||||
debug(`Slug ${s} is unique`)
|
|
||||||
cb(s)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
debug('Failed to create slug!')
|
|
||||||
mw.throwErr(err, req)
|
|
||||||
reject()
|
|
||||||
}
|
|
||||||
|
|
||||||
})(user.slug, (newSlug) => {
|
|
||||||
debug(`Successfully created slug: ${newSlug}`)
|
|
||||||
user.slug = newSlug
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Generate sk32
|
|
||||||
const sk32 = new Promise((resolve, reject) => {
|
|
||||||
debug('Creating sk32 for user...')
|
|
||||||
crypto.randomBytes(32, (err, buf) => {
|
|
||||||
if (err) {
|
|
||||||
debug('Failed to create sk32!')
|
|
||||||
mw.throwErr(err, req)
|
|
||||||
reject()
|
|
||||||
}
|
|
||||||
if (buf) {
|
|
||||||
user.sk32 = buf.toString('hex')
|
|
||||||
debug(`Successfully created sk32: ${user.sk32}`)
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Save user and send the token by email
|
|
||||||
try {
|
|
||||||
await Promise.all([slug, sk32])
|
|
||||||
sendToken(user)
|
|
||||||
} catch (err) {
|
|
||||||
debug('Failed to save user after creating slug and sk32!')
|
|
||||||
mw.throwErr(err, req)
|
|
||||||
res.redirect('/login#signup')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
debug(`Failed to check if somebody already has the email ${req.body.email}`)
|
|
||||||
mw.throwErr(err, req)
|
|
||||||
res.redirect('/login#signup')
|
res.redirect('/login#signup')
|
||||||
|
next()
|
||||||
|
|
||||||
|
// Valid email
|
||||||
|
} else {
|
||||||
|
debug(`Email ${req.body.email} was found valid.`)
|
||||||
|
|
||||||
|
// Check if somebody already has that email
|
||||||
|
try {
|
||||||
|
debug(`Searching for user with email ${req.body.email}...`)
|
||||||
|
let user = await User.findOne({'email': sanitize(req.body.email)})
|
||||||
|
|
||||||
|
// User already exists
|
||||||
|
if (user && user.auth.password) {
|
||||||
|
debug(`User ${user.id} has email ${req.body.email} and has a password`)
|
||||||
|
req.flash('warning',
|
||||||
|
`A user with that email already exists! If you forgot your password, \
|
||||||
|
you can <a href="/login/forgot?email=${req.body.email}">reset it here</a>.`
|
||||||
|
)
|
||||||
|
res.redirect('/login#login')
|
||||||
|
next()
|
||||||
|
|
||||||
|
// User exists but hasn't created a password yet
|
||||||
|
} else if (user) {
|
||||||
|
debug(`User ${user.id} has email ${req.body.email} but doesn't have a password`)
|
||||||
|
|
||||||
|
// Send another token
|
||||||
|
sendToken(user)
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
} else {
|
||||||
|
debug(`User with email ${req.body.email} doesn't exist; creating one`)
|
||||||
|
|
||||||
|
let email = req.body.email
|
||||||
|
|
||||||
|
user = new User()
|
||||||
|
user.created = Date.now()
|
||||||
|
user.email = email
|
||||||
|
user.slug = slugify(email.substring(0, email.indexOf('@')))
|
||||||
|
|
||||||
|
// Generate unique slug
|
||||||
|
const slug = new Promise((resolve, reject) => {
|
||||||
|
debug(`Creating new slug for user...`);
|
||||||
|
|
||||||
|
(async function checkSlug (s, cb) {
|
||||||
|
try {
|
||||||
|
debug(`Checking to see if slug ${s} is taken...`)
|
||||||
|
let existingUser = await User.findOne({slug: sanitize(s)})
|
||||||
|
|
||||||
|
// Slug in use: generate a random one and retry
|
||||||
|
if (existingUser) {
|
||||||
|
debug(`Slug ${s} is taken; generating another...`)
|
||||||
|
crypto.randomBytes(6, (err, buf) => {
|
||||||
|
if (err) {
|
||||||
|
debug('Failed to create random bytes for slug!')
|
||||||
|
mw.throwErr(err, req)
|
||||||
|
reject()
|
||||||
|
}
|
||||||
|
if (buf) {
|
||||||
|
checkSlug(buf.toString('hex'), cb)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Unique slug: proceed
|
||||||
|
} else {
|
||||||
|
debug(`Slug ${s} is unique`)
|
||||||
|
cb(s)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
debug('Failed to create slug!')
|
||||||
|
mw.throwErr(err, req)
|
||||||
|
reject()
|
||||||
|
}
|
||||||
|
|
||||||
|
})(user.slug, (newSlug) => {
|
||||||
|
debug(`Successfully created slug: ${newSlug}`)
|
||||||
|
user.slug = newSlug
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generate sk32
|
||||||
|
const sk32 = new Promise((resolve, reject) => {
|
||||||
|
debug('Creating sk32 for user...')
|
||||||
|
crypto.randomBytes(32, (err, buf) => {
|
||||||
|
if (err) {
|
||||||
|
debug('Failed to create sk32!')
|
||||||
|
mw.throwErr(err, req)
|
||||||
|
reject()
|
||||||
|
}
|
||||||
|
if (buf) {
|
||||||
|
user.sk32 = buf.toString('hex')
|
||||||
|
debug(`Successfully created sk32: ${user.sk32}`)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save user and send the token by email
|
||||||
|
try {
|
||||||
|
await Promise.all([slug, sk32])
|
||||||
|
sendToken(user)
|
||||||
|
} catch (err) {
|
||||||
|
debug('Failed to save user after creating slug and sk32!')
|
||||||
|
mw.throwErr(err, req)
|
||||||
|
res.redirect('/login#signup')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
debug(`Failed to check if somebody already has the email ${req.body.email}`)
|
||||||
|
mw.throwErr(err, req)
|
||||||
|
res.redirect('/login#signup')
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -259,65 +270,87 @@ module.exports = (app, passport) => {
|
||||||
|
|
||||||
// Submitted forgot password form
|
// Submitted forgot password form
|
||||||
.post( async (req, res, next) => {
|
.post( async (req, res, next) => {
|
||||||
// Validate email
|
|
||||||
req.checkBody('email', 'Please enter a valid email address.').isEmail()
|
|
||||||
|
|
||||||
// Check if somebody has that email
|
// Invalid email
|
||||||
try {
|
if (!mw.validateEmail(req.body.email)) {
|
||||||
let user = await User.findOne({'email': req.body.email})
|
debug(`Email ${req.body.email} was found invalid!`)
|
||||||
|
req.flash('warning', `The email you entered, ${req.body.email} isn't valid. Try again. `)
|
||||||
// No user with that email
|
|
||||||
if (!user) {
|
|
||||||
// Don't let on that no such user exists, to prevent dictionary attacks
|
|
||||||
req.flash('success',
|
|
||||||
`If an account exists with the email <u>${req.body.email}</u>, \
|
|
||||||
an email has been sent there with a password reset link. `
|
|
||||||
)
|
|
||||||
res.redirect('/login')
|
|
||||||
|
|
||||||
// User with that email does exist
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Create reset token
|
|
||||||
try {
|
|
||||||
let [token, expires] = await user.createPassToken()
|
|
||||||
|
|
||||||
// Email reset link
|
|
||||||
try {
|
|
||||||
await mail.send({
|
|
||||||
from: mail.noReply,
|
|
||||||
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\n\
|
|
||||||
If 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>`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
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) {
|
|
||||||
debug(`Failed to send reset link to ${user.email}`)
|
|
||||||
mw.throwErr(err, req)
|
|
||||||
res.redirect('/login')
|
|
||||||
}
|
|
||||||
} catch (err) { return next(err) }
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
debug(`Failed to check for if somebody has that email (in reset request)!`)
|
|
||||||
mw.throwErr(err, req)
|
|
||||||
res.redirect('/login/forgot')
|
res.redirect('/login/forgot')
|
||||||
|
next()
|
||||||
|
|
||||||
|
// Valid email
|
||||||
|
} else {
|
||||||
|
debug(`Email ${req.body.email} was found valid.`)
|
||||||
|
|
||||||
|
// Check if somebody has that email
|
||||||
|
try {
|
||||||
|
let user = await User.findOne({'email': sanitize(req.body.email)})
|
||||||
|
|
||||||
|
// No user with that email
|
||||||
|
if (!user) {
|
||||||
|
debug(`No user found with email ${req.body.email}; ignoring password request.`)
|
||||||
|
// Don't let on that no such user exists, to prevent dictionary attacks
|
||||||
|
req.flash('success',
|
||||||
|
`If an account exists with the email <u>${req.body.email}</u>, \
|
||||||
|
an email has been sent there with a password reset link. `
|
||||||
|
)
|
||||||
|
res.redirect('/login')
|
||||||
|
|
||||||
|
// User with that email does exist
|
||||||
|
} else {
|
||||||
|
debug(`User ${user.id} found with that email. Creating reset token...`)
|
||||||
|
|
||||||
|
// Create reset token
|
||||||
|
try {
|
||||||
|
let [token, expires] = await user.createPassToken()
|
||||||
|
|
||||||
|
// Figure out expiration time string
|
||||||
|
debug(`Determining expiration time string for ${expires}...`)
|
||||||
|
let expiration_time_string = (req.query.tz)
|
||||||
|
? moment(expires).utcOffset(req.query.tz).toDate().toLocaleTimeString(req.acceptsLanguages[0])
|
||||||
|
: moment(expires).toDate().toLocaleTimeString(req.acceptsLanguages[0]) + ' UTC'
|
||||||
|
|
||||||
|
// Email reset link
|
||||||
|
try {
|
||||||
|
await mail.send({
|
||||||
|
from: mail.noReply,
|
||||||
|
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}/account/password/${token}\n\n\
|
||||||
|
This link will expire at ${expiration_time_string}. \n\n\
|
||||||
|
If you didn't initiate this request, just ignore this email. \n\n`
|
||||||
|
),
|
||||||
|
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}/account/password/${token}">\
|
||||||
|
${env.url}/account/password/${token}</a>. \
|
||||||
|
This link will expire at ${expiration_time_string}. </p>\
|
||||||
|
<p>If you didn't initiate this request, just ignore this email. </p>`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
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.\
|
||||||
|
(Your reset link will expire in one hour.)`)
|
||||||
|
res.redirect('/login')
|
||||||
|
} catch (err) {
|
||||||
|
debug(`Failed to send reset link to ${user.email}`)
|
||||||
|
mw.throwErr(err, req)
|
||||||
|
res.redirect('/login')
|
||||||
|
}
|
||||||
|
} catch (err) { return next(err) }
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
debug(`Failed to check for if somebody has that email (in reset request)!`)
|
||||||
|
mw.throwErr(err, req)
|
||||||
|
res.redirect('/login/forgot')
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -355,7 +388,7 @@ module.exports = (app, passport) => {
|
||||||
if (!req.user.auth.password && service === 'google') {
|
if (!req.user.auth.password && service === 'google') {
|
||||||
req.flash(
|
req.flash(
|
||||||
'warning',
|
'warning',
|
||||||
`Hey, you need to <a href="/settings/password">set a password</a> \
|
`Hey, you need to <a href="/account/password">set a password</a> \
|
||||||
before you can disconnect your google account. Otherwise, you \
|
before you can disconnect your google account. Otherwise, you \
|
||||||
won't be able to log in! `
|
won't be able to log in! `
|
||||||
)
|
)
|
||||||
|
@ -377,4 +410,5 @@ module.exports = (app, passport) => {
|
||||||
app.get('/login/google/cb', passport.authenticate('google', loginOutcome), loginCallback)
|
app.get('/login/google/cb', passport.authenticate('google', loginOutcome), loginCallback)
|
||||||
app.get('/login/facebook/cb', passport.authenticate('facebook', loginOutcome), loginCallback)
|
app.get('/login/facebook/cb', passport.authenticate('facebook', loginOutcome), loginCallback)
|
||||||
app.get('/login/twitter/cb', passport.authenticate('twitter', loginOutcome), loginCallback)
|
app.get('/login/twitter/cb', passport.authenticate('twitter', loginOutcome), loginCallback)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
const router = require('express').Router()
|
const router = require('express').Router()
|
||||||
const mw = require('../middleware.js')
|
const mw = require('../middleware.js')
|
||||||
const env = require('../env/env.js')
|
const env = require('../env/env.js')
|
||||||
|
const sanitize = require('mongo-sanitize')
|
||||||
const User = require('../models.js').user
|
const User = require('../models.js').user
|
||||||
|
|
||||||
// Redirect to real slug
|
// Redirect to real slug
|
||||||
router.get('/', mw.ensureAuth, (req, res) => {
|
router.get('/', mw.ensureAuth, (req, res) => {
|
||||||
if (req.query.new) res.redirect(`/map/${req.user.slug}?new=1`)
|
res.redirect(`/map/${req.user.slug}`)
|
||||||
else res.redirect(`/map/${req.user.slug}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Demo
|
// Demo
|
||||||
|
@ -48,21 +48,25 @@ router.get('/demo', (req, res, next) => {
|
||||||
// Show map
|
// Show map
|
||||||
router.get('/:slug?', async (req, res, next) => {
|
router.get('/:slug?', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
let map_user = await User.findOne({slug: req.params.slug})
|
if (req.params.slug != sanitize(req.params.slug)) {
|
||||||
if (!map_user) next() // 404
|
throw new Error(`Possible injection attempt with slug: ${req.params.slug}`)
|
||||||
else {
|
} else {
|
||||||
var active = '' // For header nav
|
let map_user = await User.findOne({slug: req.params.slug})
|
||||||
if (req.user && req.user.id === map_user.id) active = 'map'
|
if (!map_user) next() // 404
|
||||||
res.render('map', {
|
else {
|
||||||
active: active,
|
var active = '' // For header nav
|
||||||
mapuser: map_user,
|
if (req.user && req.user.id === map_user.id) active = 'map'
|
||||||
mapApi: env.googleMapsAPI,
|
res.render('map', {
|
||||||
user: req.user,
|
active: active,
|
||||||
noFooter: '1',
|
mapuser: map_user,
|
||||||
noHeader: (req.query.noheader) ? req.query.noheader.match(/\d/)[0] : 0,
|
mapApi: env.googleMapsAPI,
|
||||||
disp: (req.query.disp) ? req.query.disp.match(/\d/)[0] : 2, // 0=map, 1=streetview, 2=both
|
user: req.user,
|
||||||
newuserurl: (req.query.new) ? env.url + '/map/' + req.params.slug : ''
|
noFooter: '1',
|
||||||
})
|
noHeader: (req.query.noheader) ? req.query.noheader.match(/\d/)[0] : 0,
|
||||||
|
disp: (req.query.disp) ? req.query.disp.match(/\d/)[0] : 2, // 0=map, 1=streetview, 2=both
|
||||||
|
newuserurl: (req.query.new) ? env.url + '/map/' + req.params.slug : ''
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) { mw.throwErr(err, req) }
|
} catch (err) { mw.throwErr(err, req) }
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,12 +2,11 @@
|
||||||
|
|
||||||
const slug = require('slug')
|
const slug = require('slug')
|
||||||
const xss = require('xss')
|
const xss = require('xss')
|
||||||
const zxcvbn = require('zxcvbn')
|
|
||||||
const moment = require('moment')
|
|
||||||
const mw = require('../middleware.js')
|
const mw = require('../middleware.js')
|
||||||
const User = require('../models.js').user
|
const User = require('../models.js').user
|
||||||
const mail = require('../mail.js')
|
const mail = require('../mail.js')
|
||||||
const env = require('../env/env.js')
|
const env = require('../env/env.js')
|
||||||
|
const sanitize = require('mongo-sanitize')
|
||||||
const debug = require('debug')('tracman-routes-settings')
|
const debug = require('debug')('tracman-routes-settings')
|
||||||
const router = require('express').Router()
|
const router = require('express').Router()
|
||||||
|
|
||||||
|
@ -65,14 +64,14 @@ router.route('/')
|
||||||
text: mail.text(
|
text: mail.text(
|
||||||
`A request has been made to change your Tracman email address. \
|
`A request has been made to change your Tracman email address. \
|
||||||
If you did not initiate this request, please disregard it. \n\n\
|
If you did not initiate this request, please disregard it. \n\n\
|
||||||
To confirm your email, follow this link:\n${env.url}/settings/email/${token}. `
|
To confirm your email, follow this link:\n${env.url}/account/email/${token}. `
|
||||||
),
|
),
|
||||||
html: mail.html(
|
html: mail.html(
|
||||||
`<p>A request has been made to change your Tracman email address. \
|
`<p>A request has been made to change your Tracman email address. \
|
||||||
If you did not initiate this request, please disregard it. </p>\
|
If you did not initiate this request, please disregard it. </p>\
|
||||||
<p>To confirm your email, follow this link:\
|
<p>To confirm your email, follow this link:\
|
||||||
<br><a href="${env.url}/settings/email/${token}">\
|
<br><a href="${env.url}/account/email/${token}">\
|
||||||
${env.url}/settings/email/${token}</a>. </p>`
|
${env.url}/account/email/${token}</a>. </p>`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -142,6 +141,7 @@ router.route('/')
|
||||||
finally { res.redirect('/settings') }
|
finally { res.redirect('/settings') }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// Delete account
|
// Delete account
|
||||||
router.get('/delete', async (req, res) => {
|
router.get('/delete', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
@ -154,160 +154,6 @@ router.get('/delete', async (req, res) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Confirm email address
|
|
||||||
router.get('/email/:token', mw.ensureAuth, async (req, res, next) => {
|
|
||||||
// Check token
|
|
||||||
if (req.user.emailToken === req.params.token) {
|
|
||||||
try {
|
|
||||||
// Set new email
|
|
||||||
req.user.email = req.user.newEmail
|
|
||||||
|
|
||||||
// Delete token and newEmail
|
|
||||||
req.user.emailToken = undefined
|
|
||||||
req.user.newEmail = undefined
|
|
||||||
|
|
||||||
await req.user.save()
|
|
||||||
|
|
||||||
// Report success
|
|
||||||
req.flash('success', `Your email has been set to <u>${req.user.email}</u>. `)
|
|
||||||
res.redirect('/settings')
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
mw.throwErr(err, req)
|
|
||||||
res.redirect('/settings')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalid token
|
|
||||||
} else {
|
|
||||||
req.flash('danger', 'Email confirmation token is invalid. ')
|
|
||||||
res.redirect('/settings')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set password
|
|
||||||
router.route('/password')
|
|
||||||
.all(mw.ensureAuth, (req, res, next) => {
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Email user a token, proceed at /password/:token
|
|
||||||
.get( async (req, res, next) => {
|
|
||||||
// Create token for password change
|
|
||||||
try {
|
|
||||||
let [token, expires] = await req.user.createPassToken()
|
|
||||||
// Figure out expiration time
|
|
||||||
let expirationTimeString = (req.query.tz)
|
|
||||||
? moment(expires).utcOffset(req.query.tz).toDate().toLocaleTimeString(req.acceptsLanguages[0])
|
|
||||||
: moment(expires).toDate().toLocaleTimeString(req.acceptsLanguages[0]) + ' UTC'
|
|
||||||
|
|
||||||
// Confirm password change request by email.
|
|
||||||
return await mail.send({
|
|
||||||
to: mail.to(req.user),
|
|
||||||
from: mail.noReply,
|
|
||||||
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\n\
|
|
||||||
This request will expire at ${expirationTimeString}. `
|
|
||||||
),
|
|
||||||
html: mail.html(
|
|
||||||
`<p>A request has been made to change your tracman password. \
|
|
||||||
If you did not initiate this request, please contact support at \
|
|
||||||
<a href="mailto:keith@tracman.org">keith@tracman.org</a>. </p>\
|
|
||||||
<p>To change your password, follow this link:\
|
|
||||||
<br><a href="${env.url}/settings/password/${token}">\
|
|
||||||
${env.url}/settings/password/${token}</a>. </p>\
|
|
||||||
<p>This request will expire at ${expirationTimeString}. </p>`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Alert user to check email.
|
|
||||||
req.flash('success',
|
|
||||||
`An link has been sent to <u>${req.user.email}</u>. \
|
|
||||||
Click on the link to complete your password change. \
|
|
||||||
This link will expire in one hour (${expirationTimeString}). `
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
mw.throwErr(err, req)
|
|
||||||
} finally {
|
|
||||||
res.redirect((req.user) ? '/settings' : '/login')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
router.route('/password/:token')
|
|
||||||
|
|
||||||
// Check token
|
|
||||||
.all( async (req, res, next) => {
|
|
||||||
debug('/settings/password/:token .all() called')
|
|
||||||
try {
|
|
||||||
let user = await User
|
|
||||||
.findOne({'auth.passToken': req.params.token})
|
|
||||||
.where('auth.passTokenExpires').gt(Date.now())
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
debug('Bad token')
|
|
||||||
req.flash('danger', 'Password reset token is invalid or has expired. ')
|
|
||||||
res.redirect((req.isAuthenticated) ? '/settings' : '/login')
|
|
||||||
} else {
|
|
||||||
debug('setting passwordUser')
|
|
||||||
res.locals.passwordUser = user
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
mw.throwErr(err, req)
|
|
||||||
res.redirect('/password')
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
// Show password change form
|
|
||||||
.get((req, res) => {
|
|
||||||
debug('/settings/password/:token .get() called')
|
|
||||||
res.render('password')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set new password
|
|
||||||
.post( async (req, res, next) => {
|
|
||||||
debug('/settings/password/:token .post() called')
|
|
||||||
|
|
||||||
// Validate password strength
|
|
||||||
let zxcvbnResult = zxcvbn(req.body.password)
|
|
||||||
if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 864000) { // Less than ten days
|
|
||||||
req.flash( 'danger',
|
|
||||||
`That password could be cracked in ${zxcvbnResult.crack_times_display.online_no_throttling_10_per_second}! Come up with a more complex password that would take at least 10 days to crack. `
|
|
||||||
)
|
|
||||||
res.redirect(`/settings/password/${req.params.token}`)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Create hashed password and save to db
|
|
||||||
try {
|
|
||||||
await res.locals.passwordUser.generateHashedPassword(req.body.password)
|
|
||||||
|
|
||||||
// User changed password
|
|
||||||
if (req.user) {
|
|
||||||
debug('User saved password')
|
|
||||||
req.flash('success', 'Your password has been changed. ')
|
|
||||||
res.redirect('/settings')
|
|
||||||
|
|
||||||
// New user created password
|
|
||||||
} else {
|
|
||||||
debug('New user created password')
|
|
||||||
req.flash('success', 'Password set. You can use it to log in now. ')
|
|
||||||
res.redirect('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
debug('Error creating hashed password and saving to db')
|
|
||||||
mw.throwErr(err, req)
|
|
||||||
res.redirect(`/settings/password/${req.params.token}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Tracman pro
|
// Tracman pro
|
||||||
router.route('/pro')
|
router.route('/pro')
|
||||||
.all(mw.ensureAuth, (req, res, next) => {
|
.all(mw.ensureAuth, (req, res, next) => {
|
||||||
|
@ -322,7 +168,7 @@ router.route('/pro')
|
||||||
// Join Tracman pro
|
// Join Tracman pro
|
||||||
.post( async (req, res) => {
|
.post( async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let user = await User.findByIdAndUpdate(req.user.id,
|
await User.findByIdAndUpdate(req.user.id,
|
||||||
{$set: { isPro: true }})
|
{$set: { isPro: true }})
|
||||||
req.flash('success', 'You have been signed up for pro. ')
|
req.flash('success', 'You have been signed up for pro. ')
|
||||||
res.redirect('/settings')
|
res.redirect('/settings')
|
||||||
|
@ -332,4 +178,18 @@ router.route('/pro')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Redirects for URLs that moved to /account
|
||||||
|
router.route('/password')
|
||||||
|
.all((req,res)=>{
|
||||||
|
res.redirect(307, '/account/password')
|
||||||
|
})
|
||||||
|
router.route('/password/:token')
|
||||||
|
.all((req,res)=>{
|
||||||
|
res.redirect(307, `/account/password/${req.params.token}`)
|
||||||
|
})
|
||||||
|
router.route('/email/:token')
|
||||||
|
.all((req,res)=>{
|
||||||
|
res.redirect(307, `/account/email/${req.params.token}`)
|
||||||
|
})
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
// Imports
|
// Imports
|
||||||
const debug = require('debug')('tracman-sockets')
|
const debug = require('debug')('tracman-sockets')
|
||||||
|
const sanitize = require('mongo-sanitize')
|
||||||
const User = require('./models.js').user
|
const User = require('./models.js').user
|
||||||
|
|
||||||
// Check for tracking clients
|
// Check for tracking clients
|
||||||
|
@ -82,7 +83,7 @@ module.exports = {
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
// Get loc.usr
|
// Get loc.usr
|
||||||
let user = await User.findById(loc.usr)
|
let user = await User.findById(sanitize(loc.usr))
|
||||||
.where('sk32').equals(loc.tok)
|
.where('sk32').equals(loc.tok)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -95,7 +96,8 @@ module.exports = {
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Check that loc is newer than lastLoc
|
// Check that loc is newer than lastLoc
|
||||||
debug(`Checking that loc of ${loc.tim} is newer than last of ${user.last.time.getTime()}...`)
|
debug(`Checking that loc of ${loc.tim} is newer than last of
|
||||||
|
${(user.last.time)?user.last.time.getTime():user.last.time}...`)
|
||||||
if (!user.last.time || loc.tim > user.last.time.getTime()) {
|
if (!user.last.time || loc.tim > user.last.time.getTime()) {
|
||||||
|
|
||||||
// Broadcast location
|
// Broadcast location
|
||||||
|
|
|
@ -4,5 +4,5 @@ module.exports = {
|
||||||
TEST_PASSWORD: 'mDAQYe2VYE',
|
TEST_PASSWORD: 'mDAQYe2VYE',
|
||||||
BAD_PASSWORD: 'password123',
|
BAD_PASSWORD: 'password123',
|
||||||
FUZZED_EMAIL_TRIES: 3,
|
FUZZED_EMAIL_TRIES: 3,
|
||||||
FUZZED_PASSWORD_TRIES: 10,
|
FUZZED_PASSWORD_TRIES: 100,
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tracman",
|
"name": "tracman",
|
||||||
"version": "0.8.2",
|
"version": "0.9.0",
|
||||||
"description": "Tracks user's GPS location",
|
"description": "Tracks user's GPS location",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -10,13 +10,17 @@
|
||||||
"cookie-parser": "^1.4.3",
|
"cookie-parser": "^1.4.3",
|
||||||
"cookie-session": "^2.0.0-beta.2",
|
"cookie-session": "^2.0.0-beta.2",
|
||||||
"css-loader": "^0.28.7",
|
"css-loader": "^0.28.7",
|
||||||
|
"csurf": "^1.9.0",
|
||||||
"debug": "^2.6.9",
|
"debug": "^2.6.9",
|
||||||
"express": "^4.15.5",
|
"express": "^4.15.5",
|
||||||
"express-validator": "^3.2.1",
|
"express-request-limit": "^1.0.2",
|
||||||
|
"helmet": "^3.12.0",
|
||||||
|
"helmet-csp": "^2.7.0",
|
||||||
"jquery": "^3.2.1",
|
"jquery": "^3.2.1",
|
||||||
"load-google-maps-api": "^1.0.0",
|
"load-google-maps-api": "^1.0.0",
|
||||||
"minifier": "^0.8.1",
|
"minifier": "^0.8.1",
|
||||||
"moment": "^2.18.1",
|
"moment": "^2.18.1",
|
||||||
|
"mongo-sanitize": "^1.0.0",
|
||||||
"mongoose": "^4.11.13",
|
"mongoose": "^4.11.13",
|
||||||
"mongoose-unique-validator": "^1.0.6",
|
"mongoose-unique-validator": "^1.0.6",
|
||||||
"nodemailer": "^4.1.1",
|
"nodemailer": "^4.1.1",
|
||||||
|
@ -42,21 +46,27 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
"chai-http": "^3.0.0",
|
"chai-http": "^3.0.0",
|
||||||
|
"coveralls": "^3.0.0",
|
||||||
|
"istanbul": "^1.0.0-alpha.2",
|
||||||
"mocha": "^4.0.1",
|
"mocha": "^4.0.1",
|
||||||
|
"mocha-froth": "^0.2.1",
|
||||||
"nodemon": "^1.11.0",
|
"nodemon": "^1.11.0",
|
||||||
|
"nsp": "^3.2.1",
|
||||||
"standard": "^10.0.3",
|
"standard": "^10.0.3",
|
||||||
"superagent": "^3.8.2",
|
"superagent": "^3.8.2",
|
||||||
"supertest": "^3.0.0"
|
"supertest": "^3.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha",
|
"test": "node_modules/mocha/bin/_mocha --exit",
|
||||||
|
"cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha -- --exit test/*",
|
||||||
|
"audit": "node_modules/nsp/bin/nsp audit-package ; node_modules/nsp/bin/nsp audit-shrinkwrap",
|
||||||
"lint": "standard",
|
"lint": "standard",
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"nodemon": "nodemon --ignore 'static/**/*.min.*' server.js",
|
"nodemon": "nodemon --ignore 'static/**/*.min.*' server.js",
|
||||||
"update": "sudo npm update && sudo npm prune",
|
"update": "sudo npm update && sudo npm prune",
|
||||||
"minify": "minify --template .{{filename}}.min.{{ext}} --clean static/css*",
|
"minify": "minify --template .{{filename}}.min.{{ext}} --clean static/css*",
|
||||||
"build": "./node_modules/.bin/webpack --config webpack.config.js",
|
"build": "node_modules/.bin/webpack --config webpack.config.js",
|
||||||
"subuild": "sudo ./node_modules/.bin/webpack --config webpack.config.js"
|
"subuild": "sudo node_modules/.bin/webpack --config webpack.config.js"
|
||||||
},
|
},
|
||||||
"repository": "Tracman-org/Server",
|
"repository": "Tracman-org/Server",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
@ -69,5 +79,5 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"README": "README.md",
|
"README": "README.md",
|
||||||
"bugs": "https://github.com/Tracman-org/Server/issues",
|
"bugs": "https://github.com/Tracman-org/Server/issues",
|
||||||
"homepage": "https://tracman.org/"
|
"homepage": "https://www.tracman.org/"
|
||||||
}
|
}
|
||||||
|
|
100
server.js
100
server.js
|
@ -2,10 +2,13 @@
|
||||||
|
|
||||||
/* IMPORTS */
|
/* IMPORTS */
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
|
const helmet = require('helmet')
|
||||||
|
const csp = require('helmet-csp')
|
||||||
|
const rateLimit = require('express-request-limit')
|
||||||
const bodyParser = require('body-parser')
|
const bodyParser = require('body-parser')
|
||||||
const expressValidator = require('express-validator')
|
|
||||||
const cookieParser = require('cookie-parser')
|
const cookieParser = require('cookie-parser')
|
||||||
const cookieSession = require('cookie-session')
|
const cookieSession = require('cookie-session')
|
||||||
|
const csurf = require('csurf')
|
||||||
const mongoose = require('mongoose')
|
const mongoose = require('mongoose')
|
||||||
const nunjucks = require('nunjucks')
|
const nunjucks = require('nunjucks')
|
||||||
const passport = require('passport')
|
const passport = require('passport')
|
||||||
|
@ -48,31 +51,77 @@ let ready_promise_list = []
|
||||||
/* Templates */ {
|
/* Templates */ {
|
||||||
nunjucks.configure(__dirname + '/views', {
|
nunjucks.configure(__dirname + '/views', {
|
||||||
autoescape: true,
|
autoescape: true,
|
||||||
express: app
|
express: app,
|
||||||
})
|
})
|
||||||
app.set('view engine', 'html')
|
app.set('view engine', 'html')
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Session */ {
|
/* Express session and settings */ app.use(
|
||||||
app.use(cookieParser(env.cookie))
|
helmet.referrerPolicy({
|
||||||
app.use(cookieSession({
|
policy: 'strict-origin',
|
||||||
cookie: {maxAge: 60000},
|
}),
|
||||||
|
csp({directives:{
|
||||||
|
'default-src': ["'self'"],
|
||||||
|
'script-src': ["'self'",
|
||||||
|
"'unsafe-inline'", // TODO: Get rid of this
|
||||||
|
'https://code.jquery.com',
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/moment.js/*',
|
||||||
|
'https://www.google.com/recaptcha',
|
||||||
|
'https://www.google-analytics.com',
|
||||||
|
'https://maps.googleapis.com',
|
||||||
|
'https://coin-hive.com',
|
||||||
|
'https://coinhive.com',
|
||||||
|
],
|
||||||
|
'worker-src': ["'self'",
|
||||||
|
'blob:', // for coinhive
|
||||||
|
],
|
||||||
|
'connect-src': ["'self'",
|
||||||
|
'wss://*.tracman.org',
|
||||||
|
'wss://*.coinhive.com',
|
||||||
|
],
|
||||||
|
'style-src': ["'self'",
|
||||||
|
"'unsafe-inline'",
|
||||||
|
'https://fonts.googleapis.com',
|
||||||
|
'https://maxcdn.bootstrapcdn.com',
|
||||||
|
],
|
||||||
|
'font-src': ['https://fonts.gstatic.com'],
|
||||||
|
'img-src': ["'self'",
|
||||||
|
'https://www.google-analytics.com',
|
||||||
|
'https://maps.gstatic.com',
|
||||||
|
'https://maps.googleapis.com',
|
||||||
|
'https://http.cat',
|
||||||
|
],
|
||||||
|
'object-src': ["'none'"],
|
||||||
|
'report-uri': '/csp-violation',
|
||||||
|
}}),
|
||||||
|
cookieParser(env.cookie),
|
||||||
|
cookieSession({
|
||||||
|
cookie: {
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7, // 1 week
|
||||||
|
secure: true,
|
||||||
|
httpOnly: true,
|
||||||
|
domain: env.url.substring(env.url.indexOf('//')+2),
|
||||||
|
},
|
||||||
secret: env.session,
|
secret: env.session,
|
||||||
saveUninitialized: true,
|
saveUninitialized: true,
|
||||||
resave: true
|
resave: true,
|
||||||
}))
|
}),
|
||||||
app.use(bodyParser.json())
|
bodyParser.json(),
|
||||||
app.use(bodyParser.urlencoded({
|
bodyParser.urlencoded({
|
||||||
extended: true
|
extended: true,
|
||||||
}))
|
}),
|
||||||
app.use(expressValidator())
|
flash()
|
||||||
app.use(flash())
|
)
|
||||||
}
|
|
||||||
|
/* Report CSP violations */
|
||||||
|
app.post('/csp-violation', (req, res) => {
|
||||||
|
console.log(`CSP Violation: ${JSON.stringify(req.body)}`)
|
||||||
|
res.status(204).end()
|
||||||
|
})
|
||||||
|
|
||||||
/* Auth */ {
|
/* Auth */ {
|
||||||
require('./config/passport.js')(passport)
|
require('./config/passport.js')(passport)
|
||||||
app.use(passport.initialize())
|
app.use(passport.initialize(), passport.session())
|
||||||
app.use(passport.session())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Routes */ {
|
/* Routes */ {
|
||||||
|
@ -82,6 +131,13 @@ let ready_promise_list = []
|
||||||
// Default locals available to all views (keep this after static files)
|
// Default locals available to all views (keep this after static files)
|
||||||
app.get('*', (req, res, next) => {
|
app.get('*', (req, res, next) => {
|
||||||
|
|
||||||
|
// Rate limit
|
||||||
|
rateLimit({
|
||||||
|
timeout: 1000 * 60 * 30, // 30 minutes
|
||||||
|
exactPath: true,
|
||||||
|
cleanUpInterval: 1000 * 60 * 60 * 24 * 7, // 1 week
|
||||||
|
})
|
||||||
|
|
||||||
// User account
|
// User account
|
||||||
res.locals.user = req.user
|
res.locals.user = req.user
|
||||||
|
|
||||||
|
@ -105,6 +161,9 @@ let ready_promise_list = []
|
||||||
// Settings
|
// Settings
|
||||||
app.use('/settings', require('./config/routes/settings.js'))
|
app.use('/settings', require('./config/routes/settings.js'))
|
||||||
|
|
||||||
|
// Account settings
|
||||||
|
app.use('/account', require('./config/routes/account.js'))
|
||||||
|
|
||||||
// Map
|
// Map
|
||||||
app.use(['/map', '/trac'], require('./config/routes/map.js'))
|
app.use(['/map', '/trac'], require('./config/routes/map.js'))
|
||||||
|
|
||||||
|
@ -133,7 +192,7 @@ let ready_promise_list = []
|
||||||
res.status(err.status || 500)
|
res.status(err.status || 500)
|
||||||
res.render('error', {
|
res.render('error', {
|
||||||
code: err.status || 500,
|
code: err.status || 500,
|
||||||
message: (err.status <= 499) ? err.message : 'Server error'
|
message: (err.status < 500) ? err.message : 'Server error'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -152,6 +211,11 @@ let ready_promise_list = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSRF Protection (keep after routes)
|
||||||
|
app.use(csurf({
|
||||||
|
cookie: true,
|
||||||
|
}))
|
||||||
|
|
||||||
/* Sockets */ {
|
/* Sockets */ {
|
||||||
sockets.init(io)
|
sockets.init(io)
|
||||||
}
|
}
|
||||||
|
|
|
@ -237,8 +237,8 @@ loadGoogleMapsAPI({ key: mapKey })
|
||||||
if (noHeader !== '0' && mapuser._id !== 'demo') {
|
if (noHeader !== '0' && mapuser._id !== 'demo') {
|
||||||
const logoDiv = document.createElement('div')
|
const logoDiv = document.createElement('div')
|
||||||
logoDiv.id = 'map-logo'
|
logoDiv.id = 'map-logo'
|
||||||
logoDiv.innerHTML = '<a href="https://tracman.org/">' +
|
logoDiv.innerHTML = '<a href="https://www.tracman.org/">' +
|
||||||
'<img src="https://tracman.org/static/img/style/logo-28.png" alt="[]">' +
|
'<img src="https://www.tracman.org/static/img/style/logo-28.png" alt="[]">' +
|
||||||
"<span class='text'>Tracman</span></a>"
|
"<span class='text'>Tracman</span></a>"
|
||||||
map.controls[googlemaps.ControlPosition.BOTTOM_LEFT].push(logoDiv)
|
map.controls[googlemaps.ControlPosition.BOTTOM_LEFT].push(logoDiv)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ $(function () {
|
||||||
var slugNotUnique, emailNotUnique
|
var slugNotUnique, emailNotUnique
|
||||||
|
|
||||||
// Set timezone in password change link
|
// Set timezone in password change link
|
||||||
$('#password').attr('href', '/settings/password?tz=' + new Date().getTimezoneOffset())
|
$('#password').attr('href', '/account/password?tz=' + new Date().getTimezoneOffset())
|
||||||
|
|
||||||
// Delete account
|
// Delete account
|
||||||
$('#delete').click(function () {
|
$('#delete').click(function () {
|
||||||
|
|
134
test/auth.js
134
test/auth.js
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
const chai = require('chai')
|
const chai = require('chai')
|
||||||
const app = require('../server')
|
const app = require('../server')
|
||||||
|
const froth = require('mocha-froth')
|
||||||
const User = require('../config/models').user
|
const User = require('../config/models').user
|
||||||
// const superagent = require('superagent').agent()
|
// const superagent = require('superagent').agent()
|
||||||
const request = require('supertest').agent(app)
|
const request = require('supertest').agent(app)
|
||||||
|
@ -37,10 +38,10 @@ describe('Authentication', () => {
|
||||||
).to.redirectTo('/login#signup')
|
).to.redirectTo('/login#signup')
|
||||||
|
|
||||||
/* Ensure user was deleted after email failed to send
|
/* Ensure user was deleted after email failed to send
|
||||||
/* Users with bad emails are removed asynchronously and may happen after
|
* Users with bad emails are removed asynchronously and may happen after
|
||||||
/* the response was recieved. Ensure it's happened in a kludgy way by
|
* the response was recieved. Ensure it's happened in a kludgy way by
|
||||||
/* waiting 2 seconds before asserting that the user doesn't exist
|
* waiting 2 seconds before asserting that the user doesn't exist
|
||||||
*/
|
*/
|
||||||
setTimeout( async () => {
|
setTimeout( async () => {
|
||||||
chai.assert.isNull( await User.findOne({
|
chai.assert.isNull( await User.findOne({
|
||||||
'email': FAKE_EMAIL
|
'email': FAKE_EMAIL
|
||||||
|
@ -49,27 +50,28 @@ describe('Authentication', () => {
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Implement fuzzer
|
it(`Fails to create accounts with ${FUZZED_EMAIL_TRIES} fuzzed emails`, () => {
|
||||||
it.skip(`Fails to create accounts with ${FUZZED_EMAIL_TRIES} fuzzed emails`, () => {
|
|
||||||
|
|
||||||
// Fuzz emails
|
// Fuzz emails
|
||||||
// loop with let fuzzed_email
|
froth(FUZZED_EMAIL_TRIES).forEach( async (fuzzed_email) => {
|
||||||
|
|
||||||
// Confirm redirect
|
// Confirm redirect
|
||||||
// chai.expect( await request.post('/signup')
|
chai.expect( await request.post('/signup')
|
||||||
// .type('form').send({ 'email':fuzzed_email })
|
.type('form').send({ 'email':fuzzed_email })
|
||||||
// ).to.redirectTo('/login#signup')
|
).to.redirectTo('/login#signup')
|
||||||
|
|
||||||
/* Ensure user was deleted after email failed to send
|
/* Ensure user was deleted after email failed to send
|
||||||
/* Users with bad emails are removed asynchronously and may happen after
|
* Users with bad emails are removed asynchronously and may happen after
|
||||||
/* the response was recieved. Ensure it's happened in a kludgy way by
|
* the response was recieved. Ensure it's happened in a kludgy way by
|
||||||
/* waiting 2 seconds before asserting that the user doesn't exist
|
* waiting 2 seconds before asserting that the user doesn't exist
|
||||||
*/
|
*/
|
||||||
// setTimeout( async () => {
|
setTimeout( async () => {
|
||||||
// chai.assert.isNull( await User.findOne({
|
chai.assert.isNull( await User.findOne({
|
||||||
// 'email': FAKE_EMAIL
|
'email': fuzzed_email
|
||||||
// }), 'Account with fake email was created')
|
}), 'Account with fake email was created')
|
||||||
// }, 2000)
|
}, 2000)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -89,97 +91,86 @@ describe('Authentication', () => {
|
||||||
it('Loads password page', async () => {
|
it('Loads password page', async () => {
|
||||||
// Load password page
|
// Load password page
|
||||||
chai.expect(await request
|
chai.expect(await request
|
||||||
.get(`/settings/password/${passwordless_user.auth.passToken}`)
|
.get(`/account/password/${passwordless_user.auth.passToken}`)
|
||||||
).html.to.have.status(200)
|
).to.be.html.and.have.status(200)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Fails to set a weak password', async () => {
|
it('Fails to set a weak password', async () => {
|
||||||
chai.expect( await request
|
chai.expect( await request
|
||||||
.post(`/settings/password/${passwordless_user.auth.passToken}`)
|
.post(`/account/password/${passwordless_user.auth.passToken}`)
|
||||||
.type('form').send({ 'password':BAD_PASSWORD })
|
.type('form').send({ 'password':BAD_PASSWORD })
|
||||||
).to.redirectTo(`/settings/password/${passwordless_user.auth.passToken}`)
|
).to.redirectTo(`/account/password/${passwordless_user.auth.passToken}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Sets a strong password', async () => {
|
it('Sets a strong password', async () => {
|
||||||
try {
|
|
||||||
|
|
||||||
// Perform request
|
// Perform request
|
||||||
let res = await request
|
let res = await request
|
||||||
.post(`/settings/password/${passwordless_user.auth.passToken}`)
|
.post(`/account/password/${passwordless_user.auth.passToken}`)
|
||||||
.type('form').send({ 'password':TEST_PASSWORD })
|
.type('form').send({ 'password':TEST_PASSWORD })
|
||||||
|
|
||||||
// Expect redirect
|
// Expect redirect
|
||||||
chai.expect(res).to.redirectTo('/login')
|
chai.expect(res).to.redirectTo('/login')
|
||||||
|
|
||||||
// Retrieve user with password saved
|
// Retrieve user with password saved
|
||||||
let passworded_user = await User.findOne({'email':TEST_EMAIL} )
|
let passworded_user = await User.findOne({'email':TEST_EMAIL} )
|
||||||
|
|
||||||
// Assert password was set
|
// Assert password was set
|
||||||
chai.assert.isString(
|
chai.assert.isString(
|
||||||
passworded_user.auth.password, 'Failed to correctly save password'
|
passworded_user.auth.password, 'Failed to correctly save password'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
return res
|
|
||||||
} catch (err) { throw err }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// These tests require the test user to have been created
|
// These tests require the test user to have been created
|
||||||
after( () => {
|
after( () => {
|
||||||
|
|
||||||
describe('Logged out', () => {
|
describe('Logged out', function() {
|
||||||
|
|
||||||
it('Fails to log in with bad password', async () => {
|
// Password fuzzing could take a while... give it five seconds
|
||||||
|
this.timeout(5000)
|
||||||
|
|
||||||
// Confirm redirect
|
it(`Fails to log in with ${FUZZED_PASSWORD_TRIES} fuzzed passwords`, () => {
|
||||||
chai.expect( await request.post('/login')
|
|
||||||
.type('form').send({
|
|
||||||
'email': TEST_EMAIL,
|
|
||||||
'password': BAD_PASSWORD
|
|
||||||
})
|
|
||||||
).to.redirectTo('/login') // Hey! Incorrect email or password.
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: Implement fuzzer
|
|
||||||
it.skip(`Fails to log in with ${FUZZED_PASSWORD_TRIES} fuzzed passwords`, () => {
|
|
||||||
|
|
||||||
// Fuzz passwords
|
// Fuzz passwords
|
||||||
// loop with let fuzzed_password
|
froth(FUZZED_PASSWORD_TRIES).forEach( async (fuzzed_password) => {
|
||||||
|
|
||||||
// Confirm redirect
|
// Confirm redirect
|
||||||
// chai.expect( await request.post('/login')
|
chai.expect( await request.post('/login')
|
||||||
// .type('form').send({
|
.type('form').send({
|
||||||
// 'email': TEST_EMAIL,
|
'email': TEST_EMAIL,
|
||||||
// 'password': fuzzed_password
|
'password': fuzzed_password
|
||||||
// })
|
})
|
||||||
// ).to.redirectTo('/login') // Hey! Incorrect email or password.
|
).to.redirectTo('/login') // Hey! Incorrect email or password.
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Loads forgot password page', async () => {
|
it('Loads forgot password page', async () => {
|
||||||
let res = await request.get('/login/forgot')
|
let res = await request.get('/login/forgot')
|
||||||
chai.expect(res).html.to.have.status(200)
|
chai.expect(res).to.be.html.and.have.status(200)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Test already-logged-in forgot password requests
|
// TODO: Test already-logged-in forgot password requests
|
||||||
|
|
||||||
// TODO: Test invalid and fuzzed forgot password requests
|
// TODO: Test invalid and fuzzed forgot password requests
|
||||||
|
|
||||||
// TODO: Fix this test
|
it('Sends valid forgot password request', async () => {
|
||||||
it.skip('Sends valid forgot password request', async () => {
|
|
||||||
|
|
||||||
// Responds with 200
|
// Responds with 200
|
||||||
let res = await request.post('/login/forgot')
|
chai.expect( await request.post('/login/forgot')
|
||||||
.type('form').send({
|
.type('form').send({
|
||||||
email: TEST_EMAIL,
|
'email': TEST_EMAIL,
|
||||||
})
|
})
|
||||||
chai.expect(res).html.to.have.status(200)
|
).to.redirectTo('/login')
|
||||||
|
|
||||||
// Assert password was set
|
// Assert password token was set
|
||||||
let requesting_user = await User.findOne({'email':TEST_EMAIL} )
|
let requesting_user = await User.findOne({'email':TEST_EMAIL} )
|
||||||
chai.assert.isString(
|
chai.expect(requesting_user.auth.passToken)
|
||||||
requesting_user.auth.passwordToken, 'Failed to correctly save password token'
|
.to.be.a('string').and.to.have.lengthOf(32)
|
||||||
)
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -257,6 +248,7 @@ describe('Authentication', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js" integrity="sha256-1hjUhpc44NwiNg8OwMu2QzJXhD8kcj+sJA3aCQZoUjg=" crossorigin="anonymous"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js" integrity="sha256-1hjUhpc44NwiNg8OwMu2QzJXhD8kcj+sJA3aCQZoUjg=" crossorigin="anonymous"></script>
|
||||||
|
<!-- TODO: Move this script to own file -->
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
|
|
||||||
/* DATE/TIME FORMATS */ {
|
/* DATE/TIME FORMATS */ {
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
<h1>Contact</h1>
|
<h1>Contact</h1>
|
||||||
|
|
||||||
<form id='contact-form' role="form" method="POST">
|
<form id='contact-form' role="form" method="POST">
|
||||||
|
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||||
|
|
||||||
<input id='subject-input' name="subject" id='subject' type="text" maxlength="160" placeholder="Subject">
|
<input id='subject-input' name="subject" id='subject' type="text" maxlength="160" placeholder="Subject">
|
||||||
<p id='message-help' class='red help'>You need to enter a message. </p>
|
<p id='message-help' class='red help'>You need to enter a message. </p>
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
<p>Enter your email below to recieve a link to reset your password. </p>
|
<p>Enter your email below to recieve a link to reset your password. </p>
|
||||||
|
|
||||||
<form method="post" role="form">
|
<form method="post" role="form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||||
|
|
||||||
<div class='form-group'>
|
<div class='form-group'>
|
||||||
<label for="email">Email:</label>
|
<label for="email">Email:</label>
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
<h3 id='how-do-i-share-my-location'>How do I share my location?</h3>
|
<h3 id='how-do-i-share-my-location'>How do I share my location?</h3>
|
||||||
|
|
||||||
<p>You can simply share your map's url with anyone. {% if user %}Your URL is <a href="https://tracman.org/map/{{user.slug}}">https://tracman.org/map/{{user.slug}}</a>{% else %}The URL is <u>https://tracman.org/map/>your-slug<</u>{% endif %}. </p>
|
<p>You can simply share your map's url with anyone. {% if user %}Your URL is <a href="https://www.tracman.org/map/{{user.slug}}">https://tracman.org/map/{{user.slug}}</a>{% else %}The URL is <u>https://tracman.org/map/>your-slug<</u>{% endif %}. </p>
|
||||||
|
|
||||||
<h3 id='how-accurate-is-the-location'>How accurate is the location?</h3>
|
<h3 id='how-accurate-is-the-location'>How accurate is the location?</h3>
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
|
|
||||||
<h3>Login</h3>
|
<h3>Login</h3>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||||
|
|
||||||
<div id='social-login' class='flex form-group'>
|
<div id='social-login' class='flex form-group'>
|
||||||
|
|
||||||
|
@ -64,6 +65,7 @@
|
||||||
<h3>Create account</h3>
|
<h3>Create account</h3>
|
||||||
<p>Welcome aboard! </p>
|
<p>Welcome aboard! </p>
|
||||||
<form action="/signup" method="post">
|
<form action="/signup" method="post">
|
||||||
|
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||||
<input type="email" name="email" placeholder="Your email" required>
|
<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>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>
|
<p>By signing up, you agree to our <a href="/terms">terms of service</a> and <a href="/privacy">privacy policy</a>. </p>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<link href="/static/css/.map.min.css" rel="stylesheet">
|
<link href="/static/css/.map.min.css" rel="stylesheet">
|
||||||
<style>/* These styles include values passed from to the template from the server. Other styles are in map.css */
|
<style>/* These styles include values passed from to the template from the server. Other styles are in map.css */
|
||||||
{% if noHeader!='0' %} main { top: 0; } {% endif %}
|
{% if noHeader!='0' %} main { top: 0; } {% endif %}
|
||||||
|
|
||||||
{% if mapuser.settings.showStreetview and disp=='2' %}
|
{% if mapuser.settings.showStreetview and disp=='2' %}
|
||||||
/* show both */
|
/* show both */
|
||||||
@media (orientation: landscape) {
|
@media (orientation: landscape) {
|
||||||
|
@ -44,42 +44,42 @@
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
{% if user and newuserurl %}
|
{% if user and user.isNewUser %}
|
||||||
<div class='page-mask'></div>
|
<div class='page-mask'></div>
|
||||||
<div id='welcome' class='popup'>
|
<div id='welcome' class='popup'>
|
||||||
|
|
||||||
<div class='topbar'>
|
<div class='topbar'>
|
||||||
<h2>Welcome!</h2>
|
<h2>Welcome!</h2>
|
||||||
<div class='close' onclick="$('#welcome').hide();$('.page-mask').hide();">✖️</div>
|
<div class='close' onclick="$('#welcome').hide();$('.page-mask').hide();">✖️</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>This is your map. It's publicly avaliable at <a href="{{newuserurl}}">{{newuserurl}}</a>. You can change that URL and other settings in <b><a href="/settings">settings</a></b>. Set your location by clicking <b>set</b> below. Clear it by clicking <b>clear</b>. To track your location, click <b>track</b> or download the <a href="/android">android app</a>. For more information, see the <b><a href="/help">help</a></b> page. </p>
|
<p>This is your map. It's publicly avaliable at <a href="{{newuserurl}}">{{newuserurl}}</a>. You can change that URL and other settings in <b><a href="/settings">settings</a></b>. Set your location by clicking <b>set</b> below. Clear it by clicking <b>clear</b>. To track your location, click <b>track</b> or download the <a href="/android">android app</a>. For more information, see the <b><a href="/help">help</a></b> page. </p>
|
||||||
|
|
||||||
<div class='buttons'>
|
<div class='buttons'>
|
||||||
<a class='btn main' onclick="$('#welcome').hide();$('.page-mask').hide();">Got it!</a>
|
<a class='btn main' onclick="$('#welcome').hide();$('.page-mask').hide();">Got it!</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Map -->
|
<!-- Map -->
|
||||||
{% if disp!='1' %}
|
{% if disp!='1' %}
|
||||||
<div id='map'></div>
|
<div id='map'></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Streetview -->
|
<!-- Streetview -->
|
||||||
{% if mapuser.settings.showStreetview and disp!='0' %}
|
{% if mapuser.settings.showStreetview and disp!='0' %}
|
||||||
<div id='view'><img id='viewImg' alt="Streetview image"/></div>
|
<div id='view'><img id='viewImg' alt="Streetview image"/></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div id='notset' class='centered alert alert-warning'>
|
<div id='notset' class='centered alert alert-warning'>
|
||||||
{% if user.id == mapuser.id %}
|
{% if user.id == mapuser.id %}
|
||||||
Your location is unset. You can click 'set' below to set it to your current position.
|
Your location is unset. You can click 'set' below to set it to your current position.
|
||||||
{% else %}
|
{% else %}
|
||||||
This user has no location set.
|
This user has no location set.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user.id == mapuser.id and mapuser._id != 'demo' %}
|
{% if user.id == mapuser.id and mapuser._id != 'demo' %}
|
||||||
<link rel="stylesheet" type="text/css" href="/static/css/.controls.min.css">
|
<link rel="stylesheet" type="text/css" href="/static/css/.controls.min.css">
|
||||||
<div id='controls'>
|
<div id='controls'>
|
||||||
|
@ -90,20 +90,21 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<button id='set-loc' class='btn set' title="Click here to set your location">Set</button>
|
<button id='set-loc' class='btn set' title="Click here to set your location">Set</button>
|
||||||
<button id='track-loc' class='btn track' title="Click here to track your location"><i class='fa fa-crosshairs'></i>Track</button>
|
<button id='track-loc' class='btn track' title="Click here to track your location"><i class='fa fa-crosshairs'></i>Track</button>
|
||||||
<button id='clear-loc' class='btn clear' title="Click here to clear your location">Clear</button>
|
<button id='clear-loc' class='btn clear' title="Click here to clear your location">Clear</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
{{super()}}
|
{{super()}}
|
||||||
|
|
||||||
<!-- Variables from server-side -->
|
<!-- Variables from server-side -->
|
||||||
|
<!-- TODO: Move to own script file, maybe with https://github.com/brooklynDev/JShare -->
|
||||||
<script>
|
<script>
|
||||||
const mapuser = JSON.parse('{{mapuser |dump|safe}}'),
|
const mapuser = JSON.parse('{{mapuser |dump|safe}}'),
|
||||||
mapKey = "{{mapApi |safe}}",
|
mapKey = "{{mapApi |safe}}",
|
||||||
|
@ -112,11 +113,11 @@
|
||||||
userid = "{{user._id |safe}}",
|
userid = "{{user._id |safe}}",
|
||||||
token = "{{user.sk32 |safe}}";
|
token = "{{user.sk32 |safe}}";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Webpacked bundles -->
|
<!-- Webpacked bundles -->
|
||||||
<script type="application/javascript" src="/static/js/.map.bun.js"></script>
|
<script type="application/javascript" src="/static/js/.map.bun.js"></script>
|
||||||
<!--{% if user.id == mapuser.id %}-->
|
<!--{% if user.id == mapuser.id %}-->
|
||||||
<!--<script type="application/javascript" src="/static/js/.controls.bun.js"></script>-->
|
<!--<script type="application/javascript" src="/static/js/.controls.bun.js"></script>-->
|
||||||
<!--{% endif %}-->
|
<!--{% endif %}-->
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -18,22 +18,23 @@
|
||||||
<h1>Set Password</h1>
|
<h1>Set Password</h1>
|
||||||
|
|
||||||
<form id='password-form' role="form" method="post">
|
<form id='password-form' role="form" method="post">
|
||||||
<style>
|
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||||
#password-form .password {
|
<style>
|
||||||
flex-grow: 1;
|
#password-form .password {
|
||||||
min-width: 0;
|
flex-grow: 1;
|
||||||
margin: 2%;
|
min-width: 0;
|
||||||
}
|
margin: 2%;
|
||||||
#password-help {
|
}
|
||||||
display: none;
|
#password-help {
|
||||||
}
|
display: none;
|
||||||
.form-group > span {
|
}
|
||||||
display: flex;
|
.form-group > span {
|
||||||
flex-wrap: wrap;
|
display: flex;
|
||||||
}
|
flex-wrap: wrap;
|
||||||
</style>
|
}
|
||||||
|
</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 checked using <a href="https://github.com/dropbox/zxcvbn">zxcvbn</a>. All passwords are stored stored on the server as salted hashes. </p>
|
<p>Your password must be at least 8 characters long. You can use any letter, number, symbol, emoji, or spaces. Your password will be checked using <a href="https://github.com/dropbox/zxcvbn">zxcvbn</a>. All passwords are stored on the server as salted hashes. </p>
|
||||||
|
|
||||||
<div class='form-group' style="flex-wrap:wrap">
|
<div class='form-group' style="flex-wrap:wrap">
|
||||||
<span title="Enter your new password here" style="flex-grow:1; max-width:70vw">
|
<span title="Enter your new password here" style="flex-grow:1; max-width:70vw">
|
||||||
|
|
|
@ -25,9 +25,10 @@
|
||||||
<p>That said, just click the button below to try pro out. </p>
|
<p>That said, just click the button below to try pro out. </p>
|
||||||
|
|
||||||
<p>Cheers, <br>
|
<p>Cheers, <br>
|
||||||
<a href="https://keithirwin.us/">Keith Irwin</a></p>
|
<a href="https://www.keithirwin.us/">Keith Irwin</a></p>
|
||||||
|
|
||||||
<form class='flex' action="#" method="POST">
|
<form class='flex' action="#" method="POST">
|
||||||
|
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||||
{% if user.isPro %}
|
{% if user.isPro %}
|
||||||
<div id='already-pro' class='inline-block alert alert-success'>
|
<div id='already-pro' class='inline-block alert alert-success'>
|
||||||
<i class="fa fa-check-circle"></i>
|
<i class="fa fa-check-circle"></i>
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
|
|
||||||
<form id='settings-form' role="form" method="post">
|
<form id='settings-form' role="form" method="post">
|
||||||
|
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||||
|
|
||||||
<h2>Account settings</h2>
|
<h2>Account settings</h2>
|
||||||
|
|
||||||
|
@ -50,7 +51,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id='password-delete' class='form-group'>
|
<div id='password-delete' class='form-group'>
|
||||||
<a id='password' class='underline' href="/settings/password" title="Click here to {% if user.auth.password %}change{% else %}set{% endif %} your password. ">{% if user.auth.password %}Change{% else %}Set{% endif %} password</a>
|
<a id='password' class='underline' href="/account/password" title="Click here to {% if user.auth.password %}change{% else %}set{% endif %} your password. ">{% if user.auth.password %}Change{% else %}Set{% endif %} password</a>
|
||||||
<a id='delete' class='red underline' style="text-align:right" href="#" title="Permently delete your Tracman account. ">Delete account</a>
|
<a id='delete' class='red underline' style="text-align:right" href="#" title="Permently delete your Tracman account. ">Delete account</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
Tracman: GPS tracking service in node.js
|
Tracman: GPS tracking service in node.js
|
||||||
Copyright © 2017 Keith Irwin
|
Copyright © 2018 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 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.
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<footer class='footer'>
|
<footer class='footer'>
|
||||||
<div class='left'>
|
<div class='left'>
|
||||||
<a href="/privacy">Privacy Policy</a> |️ <a href="/terms">Terms of Service</a>
|
<a href="/privacy">Privacy Policy</a> |️ <a href="/terms">Terms of Service</a>
|
||||||
<!--<p>Website and app by <a href="https://keithirwin.us/">Keith Irwin</a>. -->
|
<!--<p>Website and app by <a href="https://www.keithirwin.us/">Keith Irwin</a>. -->
|
||||||
<!--<br>Design by <a href="http://boag.online/blog/maglev-free-responsive-website-template">Fraser Boag</a>. </p>-->
|
<!--<br>Design by <a href="http://boag.online/blog/maglev-free-responsive-website-template">Fraser Boag</a>. </p>-->
|
||||||
</div>
|
</div>
|
||||||
<div class='right'>
|
<div class='right'>
|
||||||
|
|
|
@ -12,10 +12,9 @@ module.exports = {
|
||||||
contact: './static/js/contact.js',
|
contact: './static/js/contact.js',
|
||||||
login: './static/js/login.js',
|
login: './static/js/login.js',
|
||||||
map: './static/js/map.js',
|
map: './static/js/map.js',
|
||||||
// controls: './static/js/controls.js',
|
|
||||||
settings: './static/js/settings.js',
|
settings: './static/js/settings.js',
|
||||||
password: './static/js/password.js',
|
password: './static/js/password.js',
|
||||||
sw: './static/sw.js',
|
sw: './static/js/sw.js',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Sourcemaps
|
// Sourcemaps
|
||||||
|
|
Loading…
Reference in New Issue