1
0
mirror of https://github.com/Foltik/Shimapan synced 2024-11-13 00:26:55 -05:00

Properly async-ify registration code

This commit is contained in:
Jack Foltz 2018-07-25 01:45:05 -04:00
parent 1fdb121260
commit ce99433afc
Signed by: foltik
GPG Key ID: 303F88F996E95541
2 changed files with 100 additions and 89 deletions

View File

@ -14,8 +14,8 @@ var InviteSchema = mongoose.Schema({
exp: Date 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); return this.model('Invite').updateOne({code: this.code}, {recipient: canonicalname, used: Date.now()}, cb);
}; };*/
module.exports = mongoose.model('Invite', InviteSchema); module.exports = mongoose.model('Invite', InviteSchema);

View File

@ -1,118 +1,129 @@
var express = require('express'); 'use strict';
var router = express.Router();
var User = require('../models/User.js'); const express = require('express');
var Invite = require('../models/Invite.js'); 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 const async = require('async');
function canonicalize(username) {
return username.normalize('NFKD').toLowerCase(); 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 const asyncMiddleware = fn =>
// Returns the invite object if valid (req, res, next) => {
function checkInvite(code, cb) { Promise.resolve(fn(req, res, next))
Invite.findOne({code: code}, function (err, invite) { .catch(next);
if (err) };
cb(err);
else if (!invite) // Normalizes, decomposes, and lowercases a utf-8 string
cb('Invalid invite code.'); const canonicalizeUsername = username => username.normalize('NFKD').toLowerCase();
else if (invite.used)
cb('Invite already used.'); // Check if a canonical name is valid
else if (invite.exp < Date.now()) async function validateUsername(username, canonicalName, sanitize) {
cb('Invite expired.'); if (canonicalName.length > 36)
else return {valid: false, message: 'Username too long.'};
cb(null, invite);
}); 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. // Query the database for a valid invite code. An error message property is set if invalid.
function registerUser(username, password, invite, sanitize, cb) { async function validateInvite(code) {
async.series([ const invite = await Invite.findOne({code: code});
function (cb) {
// Canonicalize and sanitize the username, checking for HTML
var canonicalName = canonicalize(username);
var sanitizedName = sanitize(canonicalName).replace(/\s/g,'');
if (sanitizedName !== canonicalName) if (!invite)
cb('Username contains invalid characters.'); return {valid: false, message: 'Invalid invite code.'};
else if (canonicalName.length > 36)
cb('Username too long.'); if (invite.used)
else return {valid: false, message: 'Invite already used.'};
cb(null);
}, if (invite.exp < Date.now())
function(cb) { return {valid: false, message: 'Invite expired.'};
async.waterfall([
function(cb) { return {valid: true, invite: invite};
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);
});
} }
// Authenticates and creates the required session variables // Authenticates and creates the required session variables
function setupSession(username, req, res, cb) { function setupSession(username, req, res, cb) {
// Body needs to contain canonical name for proper authentication // 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 () { passport.authenticate('local')(req, res, function () {
req.session.save(function (err) { req.session.save(function (err) {
if (!err) { if (!err) {
req.session.passport.username = username; req.session.passport.username = username;
req.session.passport.canonicalname = canonicalize(username); req.session.passport.canonicalname = canonicalizeUsername(username);
} }
cb(err); cb(err);
}); });
}); });
} }
router.post('/register', function (req, res) { router.post('/register', asyncMiddleware(async (req, res, next) => {
async.waterfall([ const reqUsername = req.body.username;
function (cb) { const reqPassword = req.body.password;
checkInvite(req.body.invite, cb); const reqInviteCode = req.body.invite;
}, const canonicalName = canonicalizeUsername(reqUsername);
function (invite, cb) {
registerUser(req.body.username, req.body.password, invite, req.sanitize, cb); // memoized verification functions
}, const checkInvite = memoize(async () => validateInvite(reqInviteCode));
function (cb) { const checkUsername = memoize(async () => validateUsername(reqUsername, canonicalName, req.sanitize));
setupSession(req.body.username, req, res, cb);
} // Validate the invite and username
], function (err) { const [inviteStatus, usernameStatus] = await Promise.all([checkInvite(), checkUsername()]);
if (err) {
res.status(401).json({'message': err}); // Make sure invite was valid
} else { if (!inviteStatus.valid)
res.status(200).json({'message': 'Registration successful.'}); 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) { router.post('/login', function (req, res, next) {
// Take 'username' from the form and canonicalize it for authentication. // 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([ async.waterfall([
function (cb) { function (cb) {
@ -128,7 +139,7 @@ router.post('/login', function (req, res, next) {
}, },
function (cb) { function (cb) {
req.session.passport.username = req.body.username; req.session.passport.username = req.body.username;
req.session.passport.canonicalname = canonicalize(req.body.username); req.session.passport.canonicalname = canonicalizeUsername(req.body.username);
cb(); cb();
} }
], function (err) { ], function (err) {