[#1314][#1489] Use collation instead of RegEx

- Add case insensitive indexes for User.email and User.username
- Update user queries by username or email so that they are case
  insensitive
This commit is contained in:
Cassie Tarakajian 2020-07-14 18:16:17 -04:00
parent 16860d76dd
commit 15ad07d5ce
7 changed files with 110 additions and 107 deletions

View file

@ -43,7 +43,7 @@ passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, don
* Authentificate using Basic Auth (Username + Api Key) * Authentificate using Basic Auth (Username + Api Key)
*/ */
passport.use(new BasicStrategy((userid, key, done) => { passport.use(new BasicStrategy((userid, key, done) => {
User.findOne({ username: userid }, (err, user) => { // eslint-disable-line consistent-return User.findOne({ username: userid }).collation({ locale: 'en', strength: 2 }).exec((err, user) => { // eslint-disable-line consistent-return
if (err) { return done(err); } if (err) { return done(err); }
if (!user) { return done(null, false); } if (!user) { return done(null, false); }
user.findMatchingKey(key, (innerErr, isMatch, keyDocument) => { user.findMatchingKey(key, (innerErr, isMatch, keyDocument) => {
@ -100,7 +100,7 @@ passport.use(new GitHubStrategy({
User.findOne({ User.findOne({
email: { $in: emails }, email: { $in: emails },
}, (findByEmailErr, existingEmailUser) => { }).collation({ locale: 'en', strength: 2 }).exec((findByEmailErr, existingEmailUser) => {
if (existingEmailUser) { if (existingEmailUser) {
existingEmailUser.email = existingEmailUser.email || primaryEmail; existingEmailUser.email = existingEmailUser.email || primaryEmail;
existingEmailUser.github = profile.id; existingEmailUser.github = profile.id;
@ -143,44 +143,45 @@ passport.use(new GoogleStrategy({
User.findOne({ User.findOne({
email: primaryEmail, email: primaryEmail,
}, (findByEmailErr, existingEmailUser) => { }).collation({ locale: 'en', strength: 2 }).exec((findByEmailErr, existingEmailUser) => {
let username = profile._json.emails[0].value.split('@')[0]; let username = profile._json.emails[0].value.split('@')[0];
User.findOne({ username }, (findByUsernameErr, existingUsernameUser) => { User.findOne({ username }).collation({ locale: 'en', strength: 2 })
if (existingUsernameUser) { .exec((findByUsernameErr, existingUsernameUser) => {
const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)]; if (existingUsernameUser) {
username = slugify(`${username} ${adj}`); const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)];
} username = slugify(`${username} ${adj}`);
// what if a username is already taken from the display name too? }
// then, append a random friendly word? // what if a username is already taken from the display name too?
if (existingEmailUser) { // then, append a random friendly word?
existingEmailUser.email = existingEmailUser.email || primaryEmail; if (existingEmailUser) {
existingEmailUser.google = profile._json.emails[0].value; existingEmailUser.email = existingEmailUser.email || primaryEmail;
existingEmailUser.username = existingEmailUser.username || username; existingEmailUser.google = profile._json.emails[0].value;
existingEmailUser.tokens.push({ kind: 'google', accessToken }); existingEmailUser.username = existingEmailUser.username || username;
existingEmailUser.name = existingEmailUser.name || profile._json.displayName; existingEmailUser.tokens.push({ kind: 'google', accessToken });
existingEmailUser.verified = User.EmailConfirmation.Verified; existingEmailUser.name = existingEmailUser.name || profile._json.displayName;
existingEmailUser.save((saveErr) => { existingEmailUser.verified = User.EmailConfirmation.Verified;
if (saveErr) { existingEmailUser.save((saveErr) => {
console.log(saveErr); if (saveErr) {
} console.log(saveErr);
done(null, existingEmailUser); }
}); done(null, existingEmailUser);
} else { });
const user = new User(); } else {
user.email = primaryEmail; const user = new User();
user.google = profile._json.emails[0].value; user.email = primaryEmail;
user.username = username; user.google = profile._json.emails[0].value;
user.tokens.push({ kind: 'google', accessToken }); user.username = username;
user.name = profile._json.displayName; user.tokens.push({ kind: 'google', accessToken });
user.verified = User.EmailConfirmation.Verified; user.name = profile._json.displayName;
user.save((saveErr) => { user.verified = User.EmailConfirmation.Verified;
if (saveErr) { user.save((saveErr) => {
console.log(saveErr); if (saveErr) {
} console.log(saveErr);
done(null, user); }
}); done(null, user);
} });
}); }
});
}); });
}); });
})); }));

View file

@ -11,7 +11,7 @@ export default function collectionForUserExists(username, collectionId, callback
} }
function findUser() { function findUser() {
return User.findOne({ username }); return User.findOne({ username }).collation({ locale: 'en', strength: 2 }).exec();
} }
function findCollection(owner) { function findCollection(owner) {

View file

@ -3,7 +3,8 @@ import User from '../../models/user';
async function getOwnerUserId(req) { async function getOwnerUserId(req) {
if (req.params.username) { if (req.params.username) {
const user = await User.findOne({ username: req.params.username }); const user =
await User.findOne({ username: req.params.username }).collation({ locale: 'en', strength: 2 }).exec();
if (user && user._id) { if (user && user._id) {
return user._id; return user._id;
} }

View file

@ -64,7 +64,7 @@ export function updateProject(req, res) {
export function getProject(req, res) { export function getProject(req, res) {
const { project_id: projectId, username } = req.params; const { project_id: projectId, username } = req.params;
User.findOne({ username }, (err, user) => { // eslint-disable-line User.findOne({ username }).collation({ locale: "en", strength: 2 }).exec((err, user) => { // eslint-disable-line
if (!user) { if (!user) {
return res.status(404).send({ message: 'Project with that username does not exist' }); return res.status(404).send({ message: 'Project with that username does not exist' });
} }
@ -141,7 +141,7 @@ export function projectExists(projectId, callback) {
} }
export function projectForUserExists(username, projectId, callback) { export function projectForUserExists(username, projectId, callback) {
User.findOne({ username }, (err, user) => { User.findOne({ username }).collation({ locale: 'en', strength: 2 }).exec((err, user) => {
if (!user) { if (!user) {
callback(false); callback(false);
return; return;

View file

@ -7,7 +7,7 @@ const UserNotFoundError = createApplicationErrorClass('UserNotFoundError');
function getProjectsForUserName(username) { function getProjectsForUserName(username) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
User.findOne({ username }, (err, user) => { User.findOne({ username }).collation({ locale: 'en', strength: 2 }).exec((err, user) => {
if (err) { if (err) {
reject(err); reject(err);
return; return;

View file

@ -1,6 +1,5 @@
import crypto from 'crypto'; import crypto from 'crypto';
import async from 'async'; import async from 'async';
import escapeStringRegexp from 'escape-string-regexp';
import User from '../models/user'; import User from '../models/user';
import mail from '../utils/mail'; import mail from '../utils/mail';
@ -31,12 +30,9 @@ const random = (done) => {
}; };
export function findUserByUsername(username, cb) { export function findUserByUsername(username, cb) {
User.findOne( User.findOne({ username }).collation({ locale: 'en', strength: 2 }).exec((err, user) => {
{ username }, cb(user);
(err, user) => { });
cb(user);
}
);
} }
export function createUser(req, res, next) { export function createUser(req, res, next) {
@ -54,51 +50,48 @@ export function createUser(req, res, next) {
verifiedTokenExpires: EMAIL_VERIFY_TOKEN_EXPIRY_TIME, verifiedTokenExpires: EMAIL_VERIFY_TOKEN_EXPIRY_TIME,
}); });
User.findOne( User.findOne({
{ $or: [
$or: [ { email },
{ email: new RegExp(`^${escapeStringRegexp(email)}$`, 'i') }, { username }
{ username: new RegExp(`^${escapeStringRegexp(username)}$`, 'i') } ]
] }).collation({ locale: 'en', strength: 2 }).exec((err, existingUser) => {
}, if (err) {
(err, existingUser) => { res.status(404).send({ error: err });
if (err) { return;
res.status(404).send({ error: err }); }
return;
}
if (existingUser) { if (existingUser) {
const fieldInUse = existingUser.email.toLowerCase() === emailLowerCase ? 'Email' : 'Username'; const fieldInUse = existingUser.email.toLowerCase() === emailLowerCase ? 'Email' : 'Username';
res.status(422).send({ error: `${fieldInUse} is in use` }); res.status(422).send({ error: `${fieldInUse} is in use` });
return;
}
user.save((saveErr) => {
if (saveErr) {
next(saveErr);
return; return;
} }
user.save((saveErr) => { req.logIn(user, (loginErr) => {
if (saveErr) { if (loginErr) {
next(saveErr); next(loginErr);
return; return;
} }
req.logIn(user, (loginErr) => {
if (loginErr) {
next(loginErr);
return;
}
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'; const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
const mailOptions = renderEmailConfirmation({ const mailOptions = renderEmailConfirmation({
body: { body: {
domain: `${protocol}://${req.headers.host}`, domain: `${protocol}://${req.headers.host}`,
link: `${protocol}://${req.headers.host}/verify?t=${token}` link: `${protocol}://${req.headers.host}/verify?t=${token}`
}, },
to: req.user.email, to: req.user.email,
}); });
mail.send(mailOptions, (mailErr, result) => { // eslint-disable-line no-unused-vars mail.send(mailOptions, (mailErr, result) => { // eslint-disable-line no-unused-vars
res.json(userResponse(req.user)); res.json(userResponse(req.user));
});
}); });
}); });
} });
); });
}); });
} }
@ -106,8 +99,8 @@ export function duplicateUserCheck(req, res) {
const checkType = req.query.check_type; const checkType = req.query.check_type;
const value = req.query[checkType]; const value = req.query[checkType];
const query = {}; const query = {};
query[checkType] = new RegExp(`^${escapeStringRegexp(value)}$`, 'i'); query[checkType] = value;
User.findOne(query, (err, user) => { User.findOne(query).collation({ locale: 'en', strength: 2 }).exec((err, user) => {
if (user) { if (user) {
return res.json({ return res.json({
exists: true, exists: true,
@ -151,18 +144,19 @@ export function resetPasswordInitiate(req, res) {
async.waterfall([ async.waterfall([
random, random,
(token, done) => { (token, done) => {
User.findOne({ email: req.body.email.toLowerCase() }, (err, user) => { User.findOne({ email: req.body.email.toLowerCase() })
if (!user) { .collation({ locale: 'en', strength: 2 }).exec((err, user) => {
res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' }); if (!user) {
return; res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' });
} return;
user.resetPasswordToken = token; }
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
user.save((saveErr) => { user.save((saveErr) => {
done(saveErr, token, user); done(saveErr, token, user);
});
}); });
});
}, },
(token, user, done) => { (token, user, done) => {
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'; const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
@ -281,7 +275,7 @@ export function updatePassword(req, res) {
} }
export function userExists(username, callback) { export function userExists(username, callback) {
User.findOne({ username: new RegExp(`^${escapeStringRegexp(username)}$`, 'i') }, (err, user) => ( User.findOne(username).collation({ locale: 'en', strength: 2 }).exec((err, user) => (
user ? callback(true) : callback(false) user ? callback(true) : callback(false)
)); ));
} }

View file

@ -1,5 +1,4 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import escapeStringRegexp from 'escape-string-regexp';
const bcrypt = require('bcrypt-nodejs'); const bcrypt = require('bcrypt-nodejs');
@ -143,16 +142,24 @@ userSchema.methods.findMatchingKey = function findMatchingKey(candidateKey, cb)
}; };
userSchema.statics.findByMailOrName = function findByMailOrName(email) { userSchema.statics.findByMailOrName = function findByMailOrName(email) {
const isEmail = email.indexOf('@') > -1;
if (isEmail) {
const query = {
email: email.toLowerCase()
};
// once emails are all lowercase, won't need to do collation
// but maybe it's not even necessary to make all emails lowercase??
return this.findOne(query).collation({ locale: 'en', strength: 2 }).exec();
}
const query = { const query = {
$or: [{ username: email
email: new RegExp(`^${escapeStringRegexp(email)}$`, 'i'),
}, {
username: new RegExp(`^${escapeStringRegexp(email)}$`, 'i'),
}],
}; };
return this.findOne(query).exec(); return this.findOne(query).collation({ locale: 'en', strength: 2 }).exec();
}; };
userSchema.statics.EmailConfirmation = EmailConfirmationStates; userSchema.statics.EmailConfirmation = EmailConfirmationStates;
userSchema.index({ username: 1 }, { collation: { locale: 'en', strength: 2 } });
userSchema.index({ email: 1 }, { collation: { locale: 'en', strength: 2 } });
export default mongoose.model('User', userSchema); export default mongoose.model('User', userSchema);