diff --git a/app/models/Invite.js b/app/models/Invite.js index dd5bcec..2159cb5 100644 --- a/app/models/Invite.js +++ b/app/models/Invite.js @@ -14,8 +14,8 @@ var InviteSchema = mongoose.Schema({ exp: Date }); -InviteSchema.methods.use = function(canonicalname, cb) { +/*InviteSchema.methods.use = function(canonicalname, cb) { return this.model('Invite').updateOne({code: this.code}, {recipient: canonicalname, used: Date.now()}, cb); -}; +};*/ module.exports = mongoose.model('Invite', InviteSchema); \ No newline at end of file diff --git a/app/routes/auth.js b/app/routes/auth.js index 381d972..d14ce2a 100644 --- a/app/routes/auth.js +++ b/app/routes/auth.js @@ -1,118 +1,129 @@ -var express = require('express'); -var router = express.Router(); +'use strict'; -var User = require('../models/User.js'); -var Invite = require('../models/Invite.js'); +const express = require('express'); +const router = express.Router(); -var passport = require('passport'); +const User = require('../models/User.js'); +const Invite = require('../models/Invite.js'); -var async = require('async'); +const passport = require('passport'); -// Normalizes, decomposes, and lowercases a unicode string -function canonicalize(username) { - return username.normalize('NFKD').toLowerCase(); +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]; + }; } -// Checks if an invite code is valid -// Returns the invite object if valid -function checkInvite(code, cb) { - Invite.findOne({code: code}, function (err, invite) { - if (err) - cb(err); - else if (!invite) - cb('Invalid invite code.'); - else if (invite.used) - cb('Invite already used.'); - else if (invite.exp < Date.now()) - cb('Invite expired.'); - else - cb(null, invite); - }); +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}; } -// Validates the username, then registers the user in the database using the given invite. -function registerUser(username, password, invite, sanitize, cb) { - async.series([ - function (cb) { - // Canonicalize and sanitize the username, checking for HTML - var canonicalName = canonicalize(username); - var sanitizedName = sanitize(canonicalName).replace(/\s/g,''); +// 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 (sanitizedName !== canonicalName) - cb('Username contains invalid characters.'); - else if (canonicalName.length > 36) - cb('Username too long.'); - else - cb(null); - }, - function(cb) { - async.waterfall([ - function(cb) { - User.count({canonicalname: canonicalize(username)}, cb); - }, - function(count, cb) { - if (count !== 0) - cb('Username in use.'); - else - cb(null); - } - ], cb); - }, - function (cb) { - User.register(new User({ - username: username, - canonicalname: canonicalize(username), - scope: invite.scope, - date: Date.now() - }), password, cb); - }, - function (cb) { - invite.use(canonicalize(username), cb); - } - ], function (err) { - cb(err); - }); + 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}; } // Authenticates and creates the required session variables function setupSession(username, req, res, cb) { // Body needs to contain canonical name for proper authentication - req.body.canonicalname = canonicalize(req.body.username); + req.body.canonicalname = canonicalizeUsername(req.body.username); passport.authenticate('local')(req, res, function () { req.session.save(function (err) { if (!err) { req.session.passport.username = username; - req.session.passport.canonicalname = canonicalize(username); + req.session.passport.canonicalname = canonicalizeUsername(username); } cb(err); }); }); } -router.post('/register', function (req, res) { - async.waterfall([ - function (cb) { - checkInvite(req.body.invite, cb); - }, - function (invite, cb) { - registerUser(req.body.username, req.body.password, invite, req.sanitize, cb); - }, - function (cb) { - setupSession(req.body.username, req, res, cb); - } - ], function (err) { - if (err) { - res.status(401).json({'message': err}); - } else { - res.status(200).json({'message': 'Registration successful.'}); - } +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() }); -}); + + // 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.'}); +})); router.post('/login', function (req, res, next) { // Take 'username' from the form and canonicalize it for authentication. - req.body.canonicalname = canonicalize(req.body.username); + req.body.canonicalname = canonicalizeUsername(req.body.username); async.waterfall([ function (cb) { @@ -128,7 +139,7 @@ router.post('/login', function (req, res, next) { }, function (cb) { req.session.passport.username = req.body.username; - req.session.passport.canonicalname = canonicalize(req.body.username); + req.session.passport.canonicalname = canonicalizeUsername(req.body.username); cb(); } ], function (err) {