2018-07-25 01:45:05 -04:00
|
|
|
'use strict';
|
2017-10-11 10:15:19 -04:00
|
|
|
|
2018-07-25 01:45:05 -04:00
|
|
|
const express = require('express');
|
|
|
|
const router = express.Router();
|
2017-10-11 10:15:19 -04:00
|
|
|
|
2018-07-25 01:45:05 -04:00
|
|
|
const User = require('../models/User.js');
|
|
|
|
const Invite = require('../models/Invite.js');
|
2017-10-11 10:15:19 -04:00
|
|
|
|
2018-07-25 01:45:05 -04:00
|
|
|
const passport = require('passport');
|
2018-01-15 15:29:32 -05:00
|
|
|
|
2018-07-25 01:45:05 -04:00
|
|
|
const async = require('async');
|
|
|
|
|
|
|
|
function memoize(fn) {
|
|
|
|
let cache = {};
|
|
|
|
|
|
|
|
return async function() {
|
|
|
|
let args = JSON.stringify(arguments);
|
|
|
|
cache[args] = cache[args] || fn.apply(this, arguments);
|
|
|
|
return cache[args];
|
|
|
|
};
|
2018-01-15 15:29:32 -05:00
|
|
|
}
|
|
|
|
|
2018-07-25 01:45:05 -04:00
|
|
|
const asyncMiddleware = fn =>
|
|
|
|
(req, res, next) => {
|
|
|
|
Promise.resolve(fn(req, res, next))
|
|
|
|
.catch(next);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Normalizes, decomposes, and lowercases a utf-8 string
|
|
|
|
const canonicalizeUsername = username => username.normalize('NFKD').toLowerCase();
|
|
|
|
|
|
|
|
// Check if a canonical name is valid
|
|
|
|
async function validateUsername(username, canonicalName, sanitize) {
|
|
|
|
if (canonicalName.length > 36)
|
|
|
|
return {valid: false, message: 'Username too long.'};
|
|
|
|
|
|
|
|
if (canonicalName !== sanitize(canonicalName).replace(/\s/g, ''))
|
|
|
|
return {valid: false, message: 'Username contains invalid characters.'};
|
|
|
|
|
|
|
|
const count = await User.countDocuments({canonicalname: canonicalName});
|
|
|
|
|
|
|
|
if (count !== 0)
|
|
|
|
return {valid: false, message: 'Username in use.'};
|
|
|
|
|
|
|
|
return {valid: true};
|
2017-10-12 12:50:02 -04:00
|
|
|
}
|
|
|
|
|
2018-07-25 01:45:05 -04:00
|
|
|
// Query the database for a valid invite code. An error message property is set if invalid.
|
|
|
|
async function validateInvite(code) {
|
|
|
|
const invite = await Invite.findOne({code: code});
|
|
|
|
|
|
|
|
if (!invite)
|
|
|
|
return {valid: false, message: 'Invalid invite code.'};
|
|
|
|
|
|
|
|
if (invite.used)
|
|
|
|
return {valid: false, message: 'Invite already used.'};
|
|
|
|
|
|
|
|
if (invite.exp < Date.now())
|
|
|
|
return {valid: false, message: 'Invite expired.'};
|
|
|
|
|
|
|
|
return {valid: true, invite: invite};
|
2017-10-11 12:55:46 -04:00
|
|
|
}
|
|
|
|
|
2018-01-15 15:29:32 -05:00
|
|
|
// Authenticates and creates the required session variables
|
|
|
|
function setupSession(username, req, res, cb) {
|
|
|
|
// Body needs to contain canonical name for proper authentication
|
2018-07-25 01:45:05 -04:00
|
|
|
req.body.canonicalname = canonicalizeUsername(req.body.username);
|
2018-01-15 15:29:32 -05:00
|
|
|
|
|
|
|
passport.authenticate('local')(req, res, function () {
|
|
|
|
req.session.save(function (err) {
|
|
|
|
if (!err) {
|
2018-01-15 17:13:35 -05:00
|
|
|
req.session.passport.username = username;
|
2018-07-25 01:45:05 -04:00
|
|
|
req.session.passport.canonicalname = canonicalizeUsername(username);
|
2018-01-15 15:29:32 -05:00
|
|
|
}
|
|
|
|
cb(err);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-07-25 01:45:05 -04:00
|
|
|
router.post('/register', asyncMiddleware(async (req, res, next) => {
|
|
|
|
const reqUsername = req.body.username;
|
|
|
|
const reqPassword = req.body.password;
|
|
|
|
const reqInviteCode = req.body.invite;
|
|
|
|
const canonicalName = canonicalizeUsername(reqUsername);
|
|
|
|
|
|
|
|
// memoized verification functions
|
|
|
|
const checkInvite = memoize(async () => validateInvite(reqInviteCode));
|
|
|
|
const checkUsername = memoize(async () => validateUsername(reqUsername, canonicalName, req.sanitize));
|
|
|
|
|
|
|
|
// Validate the invite and username
|
|
|
|
const [inviteStatus, usernameStatus] = await Promise.all([checkInvite(), checkUsername()]);
|
|
|
|
|
|
|
|
// Make sure invite was valid
|
|
|
|
if (!inviteStatus.valid)
|
|
|
|
return res.status(422).json({'message': inviteStatus.message});
|
|
|
|
|
|
|
|
// Make sure the username was valid
|
|
|
|
if (!usernameStatus.valid)
|
|
|
|
return res.status(422).json({'message': usernameStatus.message});
|
|
|
|
|
|
|
|
// Create the new user object
|
|
|
|
const user = new User({
|
|
|
|
username: reqUsername,
|
|
|
|
canonicalname: canonicalName,
|
|
|
|
scope: inviteStatus.invite.scope,
|
|
|
|
date: Date.now()
|
2017-10-11 12:55:46 -04:00
|
|
|
});
|
2018-07-25 01:45:05 -04:00
|
|
|
|
|
|
|
// memoized password setting, user saving, and invite updating functions
|
|
|
|
const updateInvite = memoize(async () =>
|
|
|
|
Invite.updateOne({code: inviteStatus.invite.code}, {recipient: canonicalName, used: Date.now()}));
|
|
|
|
const setPassword = memoize(async () => user.setPassword(reqPassword));
|
|
|
|
const saveUser = memoize(async () => {
|
|
|
|
await setPassword();
|
|
|
|
return user.save();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Set the password, save the user, and update the invite code
|
|
|
|
await Promise.all([updateInvite(), setPassword(), saveUser()]);
|
|
|
|
|
|
|
|
res.status(200).json({'message': 'Registration successful.'});
|
|
|
|
}));
|
2017-10-11 10:15:19 -04:00
|
|
|
|
2017-10-18 13:31:08 -04:00
|
|
|
router.post('/login', function (req, res, next) {
|
2018-01-15 15:29:32 -05:00
|
|
|
// Take 'username' from the form and canonicalize it for authentication.
|
2018-07-25 01:45:05 -04:00
|
|
|
req.body.canonicalname = canonicalizeUsername(req.body.username);
|
2018-01-15 15:29:32 -05:00
|
|
|
|
|
|
|
async.waterfall([
|
|
|
|
function (cb) {
|
|
|
|
passport.authenticate('local', function(err, user, info) {
|
|
|
|
cb(err, user, info);
|
|
|
|
})(req, res, next);
|
|
|
|
},
|
|
|
|
function (user, info, cb) {
|
|
|
|
if (!user)
|
|
|
|
cb(info);
|
|
|
|
else
|
|
|
|
req.logIn(user, cb);
|
|
|
|
},
|
|
|
|
function (cb) {
|
2018-01-15 17:13:35 -05:00
|
|
|
req.session.passport.username = req.body.username;
|
2018-07-25 01:45:05 -04:00
|
|
|
req.session.passport.canonicalname = canonicalizeUsername(req.body.username);
|
2018-01-15 15:29:32 -05:00
|
|
|
cb();
|
|
|
|
}
|
|
|
|
], function (err) {
|
|
|
|
if (err)
|
|
|
|
res.status(401).json({'message': err});
|
|
|
|
else
|
|
|
|
res.status(200).json({'message': 'Login successful.'});
|
|
|
|
});
|
2017-10-11 10:15:19 -04:00
|
|
|
});
|
|
|
|
|
2017-10-18 13:31:08 -04:00
|
|
|
router.get('/logout', function (req, res) {
|
|
|
|
req.logout();
|
|
|
|
res.status(200).json({'message': 'Logged out.'});
|
2017-10-14 17:49:11 -04:00
|
|
|
});
|
|
|
|
|
2018-01-15 15:29:32 -05:00
|
|
|
router.get('/session', function (req, res) {
|
2018-01-15 17:13:35 -05:00
|
|
|
console.log(req.session.passport);
|
2018-01-15 15:29:32 -05:00
|
|
|
if (req.session.passport.canonicalname) {
|
|
|
|
User.findOne({canonicalname: req.session.passport.canonicalname}, function (err, user) {
|
|
|
|
res.status(200).json({
|
|
|
|
username: user.username,
|
|
|
|
canonicalname: user.canonicalname,
|
|
|
|
scope: user.scope
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
res.status(401).json({'message': 'Unauthorized.'});
|
|
|
|
}
|
2017-10-18 13:31:08 -04:00
|
|
|
});
|
2017-10-11 10:15:19 -04:00
|
|
|
|
|
|
|
module.exports = router;
|