[#1314][#1489] Add static methods to user model

- Add new static methods to user model
  - `findByEmailAndUsername`
  - renames `findByMailOrName` to `findByEmailOrUsername`
  - `findByUsername`
  - `findByEmail`
- Reverts case insensitive behavior for username
This commit is contained in:
Cassie Tarakajian 2020-07-15 17:33:11 -04:00
parent 15ad07d5ce
commit 6259f58233
7 changed files with 132 additions and 79 deletions

View file

@ -24,7 +24,7 @@ passport.deserializeUser((id, done) => {
* Sign in using Email/Username and Password. * Sign in using Email/Username and Password.
*/ */
passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => { passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
User.findByMailOrName(email) User.findByEmailOrUsername(email)
.then((user) => { // eslint-disable-line consistent-return .then((user) => { // eslint-disable-line consistent-return
if (!user) { if (!user) {
return done(null, false, { msg: `Email ${email} not found.` }); return done(null, false, { msg: `Email ${email} not found.` });
@ -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 }).collation({ locale: 'en', strength: 2 }).exec((err, user) => { // eslint-disable-line consistent-return User.findByUsername(userid, (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) => {
@ -98,9 +98,7 @@ passport.use(new GitHubStrategy({
const emails = getVerifiedEmails(profile.emails); const emails = getVerifiedEmails(profile.emails);
const primaryEmail = getPrimaryEmail(profile.emails); const primaryEmail = getPrimaryEmail(profile.emails);
User.findOne({ User.findByEmail(emails, (findByEmailErr, existingEmailUser) => {
email: { $in: emails },
}).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;
@ -141,47 +139,44 @@ passport.use(new GoogleStrategy({
const primaryEmail = profile._json.emails[0].value; const primaryEmail = profile._json.emails[0].value;
User.findOne({ User.findByEmail(primaryEmail, (findByEmailErr, existingEmailUser) => {
email: primaryEmail,
}).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 }).collation({ locale: 'en', strength: 2 }) User.findByUsername(username, (findByUsernameErr, existingUsernameUser) => {
.exec((findByUsernameErr, existingUsernameUser) => { if (existingUsernameUser) {
if (existingUsernameUser) { const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)];
const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)]; username = slugify(`${username} ${adj}`);
username = slugify(`${username} ${adj}`); }
} // what if a username is already taken from the display name too?
// what if a username is already taken from the display name too? // then, append a random friendly word?
// then, append a random friendly word? if (existingEmailUser) {
if (existingEmailUser) { existingEmailUser.email = existingEmailUser.email || primaryEmail;
existingEmailUser.email = existingEmailUser.email || primaryEmail; existingEmailUser.google = profile._json.emails[0].value;
existingEmailUser.google = profile._json.emails[0].value; existingEmailUser.username = existingEmailUser.username || username;
existingEmailUser.username = existingEmailUser.username || username; existingEmailUser.tokens.push({ kind: 'google', accessToken });
existingEmailUser.tokens.push({ kind: 'google', accessToken }); existingEmailUser.name = existingEmailUser.name || profile._json.displayName;
existingEmailUser.name = existingEmailUser.name || profile._json.displayName; existingEmailUser.verified = User.EmailConfirmation.Verified;
existingEmailUser.verified = User.EmailConfirmation.Verified; existingEmailUser.save((saveErr) => {
existingEmailUser.save((saveErr) => { if (saveErr) {
if (saveErr) { console.log(saveErr);
console.log(saveErr); }
} done(null, existingEmailUser);
done(null, existingEmailUser); });
}); } else {
} else { const user = new User();
const user = new User(); user.email = primaryEmail;
user.email = primaryEmail; user.google = profile._json.emails[0].value;
user.google = profile._json.emails[0].value; user.username = username;
user.username = username; user.tokens.push({ kind: 'google', accessToken });
user.tokens.push({ kind: 'google', accessToken }); user.name = profile._json.displayName;
user.name = profile._json.displayName; user.verified = User.EmailConfirmation.Verified;
user.verified = User.EmailConfirmation.Verified; user.save((saveErr) => {
user.save((saveErr) => { if (saveErr) {
if (saveErr) { console.log(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 }).collation({ locale: 'en', strength: 2 }).exec(); return User.findByUsername(username);
} }
function findCollection(owner) { function findCollection(owner) {

View file

@ -4,7 +4,7 @@ import User from '../../models/user';
async function getOwnerUserId(req) { async function getOwnerUserId(req) {
if (req.params.username) { if (req.params.username) {
const user = const user =
await User.findOne({ username: req.params.username }).collation({ locale: 'en', strength: 2 }).exec(); await User.findByUsername(req.params.username);
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 }).collation({ locale: "en", strength: 2 }).exec((err, user) => { // eslint-disable-line User.findByUsername(username, (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 }).collation({ locale: 'en', strength: 2 }).exec((err, user) => { User.findByUsername(username, (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 }).collation({ locale: 'en', strength: 2 }).exec((err, user) => { User.findByUsername(username, (err, user) => {
if (err) { if (err) {
reject(err); reject(err);
return; return;

View file

@ -30,7 +30,7 @@ const random = (done) => {
}; };
export function findUserByUsername(username, cb) { export function findUserByUsername(username, cb) {
User.findOne({ username }).collation({ locale: 'en', strength: 2 }).exec((err, user) => { User.findByUsername(username, (err, user) => {
cb(user); cb(user);
}); });
} }
@ -50,12 +50,7 @@ export function createUser(req, res, next) {
verifiedTokenExpires: EMAIL_VERIFY_TOKEN_EXPIRY_TIME, verifiedTokenExpires: EMAIL_VERIFY_TOKEN_EXPIRY_TIME,
}); });
User.findOne({ User.findByEmailAndUsername(email, username, (err, existingUser) => {
$or: [
{ email },
{ username }
]
}).collation({ locale: 'en', strength: 2 }).exec((err, existingUser) => {
if (err) { if (err) {
res.status(404).send({ error: err }); res.status(404).send({ error: err });
return; return;
@ -100,6 +95,9 @@ export function duplicateUserCheck(req, res) {
const value = req.query[checkType]; const value = req.query[checkType];
const query = {}; const query = {};
query[checkType] = value; query[checkType] = value;
// Don't want to use findByEmailOrUsername here, because in this case we do
// want to use case-insensitive search for usernames to prevent username
// duplicates, which overrides the default behavior.
User.findOne(query).collation({ locale: 'en', strength: 2 }).exec((err, user) => { User.findOne(query).collation({ locale: 'en', strength: 2 }).exec((err, user) => {
if (user) { if (user) {
return res.json({ return res.json({
@ -144,19 +142,18 @@ export function resetPasswordInitiate(req, res) {
async.waterfall([ async.waterfall([
random, random,
(token, done) => { (token, done) => {
User.findOne({ email: req.body.email.toLowerCase() }) User.findByEmail(req.body.email, (err, user) => {
.collation({ locale: 'en', strength: 2 }).exec((err, user) => { if (!user) {
if (!user) { res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' });
res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' }); return;
return; }
} user.resetPasswordToken = token;
user.resetPasswordToken = token; user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
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';
@ -275,7 +272,7 @@ export function updatePassword(req, res) {
} }
export function userExists(username, callback) { export function userExists(username, callback) {
User.findOne(username).collation({ locale: 'en', strength: 2 }).exec((err, user) => ( User.findByUsername(username, (err, user) => (
user ? callback(true) : callback(false) user ? callback(true) : callback(false)
)); ));
} }

View file

@ -141,20 +141,81 @@ userSchema.methods.findMatchingKey = function findMatchingKey(candidateKey, cb)
if (!foundOne) cb('Matching API key not found !', false, null); if (!foundOne) cb('Matching API key not found !', false, null);
}; };
userSchema.statics.findByMailOrName = function findByMailOrName(email) { /**
const isEmail = email.indexOf('@') > -1; *
if (isEmail) { * Queries User collection by email and returns one User document.
const query = { *
email: email.toLowerCase() * @param {string|string[]} email - Email string or array of email strings
* @callback [cb] - Optional error-first callback that passes User document
* @return {Promise<Object>} - Returns Promise fulfilled by User document
*/
userSchema.statics.findByEmail = function findByEmail(email, cb) {
let query;
if (Array.isArray(email)) {
query = {
email: { $in: email }
};
} else {
query = {
email
}; };
// 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();
} }
// Email addresses should be case-insensitive unique
// In MongoDB, you must use collation in order to do a case-insensitive query
return this.findOne(query).collation({ locale: 'en', strength: 2 }).exec(cb);
};
/**
*
* Queries User collection by username and returns one User document.
*
* @param {string} username - Username string
* @callback [cb] - Optional error-first callback that passes User document
* @return {Promise<Object>} - Returns Promise fulfilled by User document
*/
userSchema.statics.findByUsername = function findByUsername(username, cb) {
const query = { const query = {
username: email username
}; };
return this.findOne(query).collation({ locale: 'en', strength: 2 }).exec(); return this.findOne(query, cb);
};
/**
*
* Queries User collection using email or username with optional callback.
* This function will determine automatically whether the data passed is
* a username or email.
*
* @param {string} value - Email or username
* @callback [cb] - Optional error-first callback that passes User document
* @return {Promise<Object>} - Returns Promise fulfilled by User document
*/
userSchema.statics.findByEmailOrUsername = function findByEmailOrUsername(value, cb) {
const isEmail = value.indexOf('@') > -1;
if (isEmail) {
return this.findByEmail(value, cb);
}
return this.findByUsername(value, cb);
};
/**
*
* Queries User collection, performing a MongoDB logical or with the email
* and username (i.e. if either one matches, will return the first document).
*
* @param {string} email
* @param {string} username
* @callback [cb] - Optional error-first callback that passes User document
* @return {Promise<Object>} - Returns Promise fulfilled by User document
*/
userSchema.statics.findByEmailAndUsername = function findByEmailAndUsername(email, username, cb) {
const query = {
$or: [
{ email },
{ username }
]
};
return this.findOne(query).collation({ locale: 'en', strength: 2 }).exec(cb);
}; };
userSchema.statics.EmailConfirmation = EmailConfirmationStates; userSchema.statics.EmailConfirmation = EmailConfirmationStates;