Merge pull request #1317 from shakti97/login-signup-issue

Fix username/email case issue in login/signup
This commit is contained in:
Cassie Tarakajian 2020-07-20 14:16:41 -04:00 committed by GitHub
commit 4506c2bde1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 558 additions and 90 deletions

1
.gitignore vendored
View file

@ -19,3 +19,4 @@ localhost.key
privkey.pem
storybook-static
duplicates.json

View file

@ -24,7 +24,7 @@ passport.deserializeUser((id, done) => {
* Sign in using Email/Username and Password.
*/
passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
User.findByMailOrName(email.toLowerCase())
User.findByEmailOrUsername(email)
.then((user) => { // eslint-disable-line consistent-return
if (!user) {
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)
*/
passport.use(new BasicStrategy((userid, key, done) => {
User.findOne({ username: userid }, (err, user) => { // eslint-disable-line consistent-return
User.findByUsername(userid, (err, user) => { // eslint-disable-line consistent-return
if (err) { return done(err); }
if (!user) { return done(null, false); }
user.findMatchingKey(key, (innerErr, isMatch, keyDocument) => {
@ -98,9 +98,7 @@ passport.use(new GitHubStrategy({
const emails = getVerifiedEmails(profile.emails);
const primaryEmail = getPrimaryEmail(profile.emails);
User.findOne({
email: { $in: emails },
}, (findByEmailErr, existingEmailUser) => {
User.findByEmail(emails, (findByEmailErr, existingEmailUser) => {
if (existingEmailUser) {
existingEmailUser.email = existingEmailUser.email || primaryEmail;
existingEmailUser.github = profile.id;
@ -141,11 +139,9 @@ passport.use(new GoogleStrategy({
const primaryEmail = profile._json.emails[0].value;
User.findOne({
email: primaryEmail,
}, (findByEmailErr, existingEmailUser) => {
User.findByEmail(primaryEmail, (findByEmailErr, existingEmailUser) => {
let username = profile._json.emails[0].value.split('@')[0];
User.findOne({ username }, (findByUsernameErr, existingUsernameUser) => {
User.findByUsername(username, (findByUsernameErr, existingUsernameUser) => {
if (existingUsernameUser) {
const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)];
username = slugify(`${username} ${adj}`);

View file

@ -1,5 +1,5 @@
export const getObjectKey = jest.mock();
export const deleteObjectsFromS3 = jest.fn();
export const signS3 = jest.fn();
export const copyObjectInS3 = jest.fn();
export const copyObjectInS3RequestHandler = jest.fn();
export const listObjectsInS3ForUser = jest.fn();

View file

@ -98,24 +98,72 @@ export function signS3(req, res) {
res.json(result);
}
export function copyObjectInS3(req, res) {
const { url } = req.body;
const objectKey = getObjectKey(url);
const fileExtension = getExtension(objectKey);
const newFilename = uuid.v4() + fileExtension;
const userId = req.user.id;
const params = {
Bucket: `${process.env.S3_BUCKET}`,
CopySource: `${process.env.S3_BUCKET}/${objectKey}`,
Key: `${userId}/${newFilename}`,
ACL: 'public-read'
};
const copy = client.copyObject(params);
copy.on('err', (err) => {
console.log(err);
export function copyObjectInS3(url, userId) {
return new Promise((resolve, reject) => {
const objectKey = getObjectKey(url);
const fileExtension = getExtension(objectKey);
const newFilename = uuid.v4() + fileExtension;
const headParams = {
Bucket: `${process.env.S3_BUCKET}`,
Key: `${objectKey}`
};
client.s3.headObject(headParams, (headErr) => {
if (headErr) {
reject(new Error(`Object with key ${process.env.S3_BUCKET}/${objectKey} does not exist.`));
return;
}
const params = {
Bucket: `${process.env.S3_BUCKET}`,
CopySource: `${process.env.S3_BUCKET}/${objectKey}`,
Key: `${userId}/${newFilename}`,
ACL: 'public-read'
};
const copy = client.copyObject(params);
copy.on('err', (err) => {
reject(err);
});
copy.on('end', (data) => {
resolve(`${s3Bucket}${userId}/${newFilename}`);
});
});
});
copy.on('end', (data) => {
res.json({ url: `${s3Bucket}${userId}/${newFilename}` });
}
export function copyObjectInS3RequestHandler(req, res) {
const { url } = req.body;
copyObjectInS3(url, req.user.id).then((newUrl) => {
res.json({ url: newUrl });
});
}
export function moveObjectToUserInS3(url, userId) {
return new Promise((resolve, reject) => {
const objectKey = getObjectKey(url);
const fileExtension = getExtension(objectKey);
const newFilename = uuid.v4() + fileExtension;
const headParams = {
Bucket: `${process.env.S3_BUCKET}`,
Key: `${objectKey}`
};
client.s3.headObject(headParams, (headErr) => {
if (headErr) {
reject(new Error(`Object with key ${process.env.S3_BUCKET}/${objectKey} does not exist.`));
return;
}
const params = {
Bucket: `${process.env.S3_BUCKET}`,
CopySource: `${process.env.S3_BUCKET}/${objectKey}`,
Key: `${userId}/${newFilename}`,
ACL: 'public-read'
};
const move = client.moveObject(params);
move.on('err', (err) => {
reject(err);
});
move.on('end', (data) => {
resolve(`${s3Bucket}${userId}/${newFilename}`);
});
});
});
}

View file

@ -11,7 +11,7 @@ export default function collectionForUserExists(username, collectionId, callback
}
function findUser() {
return User.findOne({ username });
return User.findByUsername(username);
}
function findCollection(owner) {

View file

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

View file

@ -64,7 +64,7 @@ export function updateProject(req, res) {
export function getProject(req, res) {
const { project_id: projectId, username } = req.params;
User.findOne({ username }, (err, user) => { // eslint-disable-line
User.findByUsername(username, (err, user) => { // eslint-disable-line
if (!user) {
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) {
User.findOne({ username }, (err, user) => {
User.findByUsername(username, (err, user) => {
if (!user) {
callback(false);
return;

View file

@ -7,7 +7,7 @@ const UserNotFoundError = createApplicationErrorClass('UserNotFoundError');
function getProjectsForUserName(username) {
return new Promise((resolve, reject) => {
User.findOne({ username }, (err, user) => {
User.findByUsername(username, (err, user) => {
if (err) {
reject(err);
return;

View file

@ -30,71 +30,63 @@ const random = (done) => {
};
export function findUserByUsername(username, cb) {
User.findOne(
{ username },
(err, user) => {
cb(user);
}
);
User.findByUsername(username, (err, user) => {
cb(user);
});
}
export function createUser(req, res, next) {
const { username, email } = req.body;
const { password } = req.body;
const emailLowerCase = email.toLowerCase();
const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours
random((tokenError, token) => {
const user = new User({
username: req.body.username,
email: req.body.email,
password: req.body.password,
username,
email: emailLowerCase,
password,
verified: User.EmailConfirmation.Sent,
verifiedToken: token,
verifiedTokenExpires: EMAIL_VERIFY_TOKEN_EXPIRY_TIME,
});
User.findOne(
{
$or: [
{ email: req.body.email },
{ username: req.body.username }
]
},
(err, existingUser) => {
if (err) {
res.status(404).send({ error: err });
return;
}
User.findByEmailAndUsername(email, username, (err, existingUser) => {
if (err) {
res.status(404).send({ error: err });
return;
}
if (existingUser) {
const fieldInUse = existingUser.email === req.body.email ? 'Email' : 'Username';
res.status(422).send({ error: `${fieldInUse} is in use` });
if (existingUser) {
const fieldInUse = existingUser.email.toLowerCase() === emailLowerCase ? 'Email' : 'Username';
res.status(422).send({ error: `${fieldInUse} is in use` });
return;
}
user.save((saveErr) => {
if (saveErr) {
next(saveErr);
return;
}
user.save((saveErr) => {
if (saveErr) {
next(saveErr);
req.logIn(user, (loginErr) => {
if (loginErr) {
next(loginErr);
return;
}
req.logIn(user, (loginErr) => {
if (loginErr) {
next(loginErr);
return;
}
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
const mailOptions = renderEmailConfirmation({
body: {
domain: `${protocol}://${req.headers.host}`,
link: `${protocol}://${req.headers.host}/verify?t=${token}`
},
to: req.user.email,
});
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
const mailOptions = renderEmailConfirmation({
body: {
domain: `${protocol}://${req.headers.host}`,
link: `${protocol}://${req.headers.host}/verify?t=${token}`
},
to: req.user.email,
});
mail.send(mailOptions, (mailErr, result) => { // eslint-disable-line no-unused-vars
res.json(userResponse(req.user));
});
mail.send(mailOptions, (mailErr, result) => { // eslint-disable-line no-unused-vars
res.json(userResponse(req.user));
});
});
}
);
});
});
});
}
@ -103,7 +95,10 @@ export function duplicateUserCheck(req, res) {
const value = req.query[checkType];
const query = {};
query[checkType] = value;
User.findOne(query, (err, user) => {
// 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) => {
if (user) {
return res.json({
exists: true,
@ -147,7 +142,7 @@ export function resetPasswordInitiate(req, res) {
async.waterfall([
random,
(token, done) => {
User.findOne({ email: req.body.email }, (err, user) => {
User.findByEmail(req.body.email, (err, user) => {
if (!user) {
res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' });
return;
@ -277,7 +272,7 @@ export function updatePassword(req, res) {
}
export function userExists(username, callback) {
User.findOne({ username }, (err, user) => (
User.findByUsername(username, (err, user) => (
user ? callback(true) : callback(false)
));
}

View file

@ -0,0 +1,237 @@
import mongoose from 'mongoose';
import fs from 'fs';
import User from '../models/user';
import Project from '../models/project';
import Collection from '../models/collection';
import { moveObjectToUserInS3, copyObjectInS3 } from '../controllers/aws.controller';
import mail from '../utils/mail';
import { renderAccountConsolidation } from '../views/mail';
const mongoConnectionString = process.env.MONGO_URL;
const { ObjectId } = mongoose.Types;
// Connect to MongoDB
mongoose.Promise = global.Promise;
mongoose.connect(mongoConnectionString, { useNewUrlParser: true, useUnifiedTopology: true });
mongoose.set('useCreateIndex', true);
mongoose.connection.on('error', () => {
console.error('MongoDB Connection Error. Please make sure that MongoDB is running.');
process.exit(1);
});
/*
* Requires the MongoDB Node.js Driver
* https://mongodb.github.io/node-mongodb-native
*/
const agg = [
{
$project: {
email: {
$toLower: [
'$email'
]
}
}
}, {
$group: {
_id: '$email',
total: {
$sum: 1
}
}
}, {
$match: {
total: {
$gt: 1
}
}
}, {
$sort: {
total: -1
}
}
];
// steps to make this work
// iterate through the results
// check if any files are on AWS
// if so, move them to the right user bucket
// then, update the user to currentUser
// then, after updating all of the projects
// also update the collections
// delete other users
// update user email so it is all lowercase
// then, send the email
// then, figure out how to iterate through all of the users.
// create list of duplicate users
// User.aggregate(agg).then((result) => {
// return fs.writeFile('duplicates.json', JSON.stringify(result), () => {
// console.log('File written.');
// process.exit(0);
// });
// });
let currentUser = null;
let duplicates = null;
fs.readFile('duplicates.json', async (err, file) => {
const result = JSON.parse(file);
for (let i = 3000; i < result.length; i += 1) {
console.log('Index: ', i);
const email = result[i]._id;
console.log(email);
await consolidateAccount(email); // eslint-disable-line
}
process.exit(0);
});
async function consolidateAccount(email) {
return User.find({ email }).collation({ locale: 'en', strength: 2 })
.sort({ createdAt: 1 }).exec()
.then((result) => {
[currentUser, ...duplicates] = result;
console.log('Current User: ', currentUser._id, ' ', currentUser.email);
duplicates = duplicates.map(dup => dup._id);
console.log('Duplicates: ', duplicates);
return Project.find({
user: { $in: duplicates }
}).exec();
}).then((sketches) => {
const saveSketchPromises = [];
sketches.forEach((sketch) => {
console.log('SketchId: ', sketch._id);
console.log('UserId: ', sketch.user);
const moveSketchFilesPromises = [];
sketch.files.forEach((file) => {
// if the file url contains sketch user
if (file.url && file.url.includes(process.env.S3_BUCKET_URL_BASE) && !file.url.includes(currentUser._id)) {
if (file.url.includes(sketch.user)) {
const fileSavePromise = moveObjectToUserInS3(file.url, currentUser._id)
.then((newUrl) => {
file.url = newUrl;
}).catch((err) => {
console.log('Move Error:');
console.log(err);
});
moveSketchFilesPromises.push(fileSavePromise);
} else {
const fileSavePromise = copyObjectInS3(file.url, currentUser._id)
.then((newUrl) => {
file.url = newUrl;
}).catch((err) => {
console.log('Copy Error:');
console.log(err);
});
moveSketchFilesPromises.push(fileSavePromise);
}
}
});
const sketchSavePromise = Promise.all(moveSketchFilesPromises).then(() => {
sketch.user = ObjectId(currentUser._id);
return sketch.save();
});
saveSketchPromises.push(sketchSavePromise);
});
return Promise.all(saveSketchPromises);
}).then(() => {
console.log('Moved and updated all sketches.');
return Collection.updateMany(
{ owner: { $in: duplicates } },
{ $set: { owner: ObjectId(currentUser.id) } }
);
}).then(() => {
console.log('Moved and updated all collections.');
return User.deleteMany({ _id: { $in: duplicates } });
}).then(() => {
console.log('Deleted other user accounts.');
currentUser.email = currentUser.email.toLowerCase();
return currentUser.save();
}).then(() => {
console.log('Migrated email to lowercase.');
// const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
const mailOptions = renderAccountConsolidation({
body: {
domain: 'https://editor.p5js.org',
username: currentUser.username,
email: currentUser.email
},
to: currentUser.email,
});
return new Promise((resolve, reject) => {
mail.send(mailOptions, (mailErr, result) => {
console.log('Sent email.');
if (mailErr) {
return reject(mailErr);
}
return resolve(result);
});
});
}).catch((err) => {
console.log(err);
process.exit(1);
});
}
// let duplicates = [
// "5ce3d936e0f9df0022d8330c",
// "5cff843f091745001e83c070",
// "5d246f5db489e6001eaee6e9"
// ];
// let currentUser = null;
// User.deleteMany({ _id: { $in: duplicates } }).then(() => {
// return User.findOne({ "email": "Silverstar09@hotmail.com" })
// }).then((result) => {
// currentUser = result;
// console.log('Deleted other user accounts.');
// currentUser.email = currentUser.email.toLowerCase();
// return currentUser.save();
// }).then(() => {
// const mailOptions = renderAccountConsolidation({
// body: {
// domain: 'https://editor.p5js.org',
// username: currentUser.username,
// email: currentUser.email
// },
// to: currentUser.email,
// });
// return new Promise((resolve, reject) => {
// mail.send(mailOptions, (mailErr, result) => {
// console.log('Sent email.');
// if (mailErr) {
// return reject(mailErr);
// }
// return resolve(result);
// });
// });
// });
// import s3 from '@auth0/s3';
// const client = s3.createClient({
// maxAsyncS3: 20,
// s3RetryCount: 3,
// s3RetryDelay: 1000,
// multipartUploadThreshold: 20971520, // this is the default (20 MB)
// multipartUploadSize: 15728640, // this is the default (15 MB)
// s3Options: {
// accessKeyId: `${process.env.AWS_ACCESS_KEY}`,
// secretAccessKey: `${process.env.AWS_SECRET_KEY}`,
// region: `${process.env.AWS_REGION}`
// },
// });
// const headParams = {
// Bucket: `${process.env.S3_BUCKET}`,
// Key: "5c9de807f6bccf0017da7927/8b9d95ae-7ddd-452a-b398-672392c4ac43.png"
// };
// client.s3.headObject(headParams, (err, data) => {
// console.log(err);
// console.log(data);
// });

View file

@ -1,7 +1,8 @@
require('@babel/register');
require('@babel/polyfill');
const path = require('path');
require('dotenv').config({ path: path.resolve('.env') });
require('./populateTotalSize');
require('dotenv').config({ path: path.resolve('.env.production') });
require('./emailConsolidation');
// require('./populateTotalSize');
// require('./moveBucket');
// require('./truncate');

View file

@ -141,17 +141,86 @@ userSchema.methods.findMatchingKey = function findMatchingKey(candidateKey, cb)
if (!foundOne) cb('Matching API key not found !', false, null);
};
userSchema.statics.findByMailOrName = function findByMailOrName(email) {
/**
*
* Queries User collection by email and returns one User document.
*
* @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
};
}
// 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 = {
$or: [{
email,
}, {
username: email,
}],
username
};
return this.findOne(query).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.index({ username: 1 }, { collation: { locale: 'en', strength: 2 } });
userSchema.index({ email: 1 }, { collation: { locale: 'en', strength: 2 } });
export default mongoose.model('User', userSchema);

View file

@ -5,7 +5,7 @@ import isAuthenticated from '../utils/isAuthenticated';
const router = new Router();
router.post('/S3/sign', isAuthenticated, AWSController.signS3);
router.post('/S3/copy', isAuthenticated, AWSController.copyObjectInS3);
router.post('/S3/copy', isAuthenticated, AWSController.copyObjectInS3RequestHandler);
router.delete('/S3/:userId?/:objectKey', isAuthenticated, AWSController.deleteObjectFromS3);
router.get('/S3/objects', AWSController.listObjectsInS3ForUserRequestHandler);

View file

@ -0,0 +1,78 @@
export default ({
domain,
headingText,
greetingText,
messageText,
username,
email,
message2Text,
resetPasswordLink,
directLinkText,
resetPasswordText,
noteText,
meta
}) => (
`
<mjml>
<mj-head>
<mj-raw>
<meta name="keywords" content="${meta.keywords}" />
<meta name="description" content="${meta.description}" />
</mj-raw>
</mj-head>
<mj-body>
<mj-container>
<mj-section>
<mj-column>
<mj-image width="192" src="${domain}/images/p5js-square-logo.png" alt="p5.js" />
<mj-divider border-color="#ed225d"></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-text font-size="20px" color="#333333" font-family="sans-serif">
${headingText}
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-text color="#333333">
${greetingText}
</mj-text>
<mj-text color="#333333">
${messageText}
</mj-text>
<mj-text color="#333333">
<span style="font-weight:bold;">Username:</span> ${username}
</mj-text>
<mj-text color="#333333">
<span style="font-weight:bold;">Email:</span> ${email}
</mj-text>
<mj-text color="#333333">
${message2Text}
</mj-text>
<mj-button background-color="#ed225d" href="${domain}/${resetPasswordLink}">
${resetPasswordText}
</mj-button>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-text color="#333333">
${directLinkText}
</mj-text>
<mj-text align="center" color="#333333"><a href="${domain}/${resetPasswordLink}">${domain}/${resetPasswordLink}</a></mj-text>
<mj-text color="#333333">
${noteText}
</mj-text>
</mj-column>
</mj-section>
</mj-container>
</mj-body>
</mjml>
`
);

View file

@ -1,5 +1,47 @@
import renderMjml from '../utils/renderMjml';
import mailLayout from './mailLayout';
import consolidationMailLayout from './consolidationMailLayout';
export const renderAccountConsolidation = (data) => {
const subject = 'p5.js Web Editor Account Consolidation';
const templateOptions = {
domain: data.body.domain,
headingText: 'Account Consolidation',
greetingText: 'Hello,',
messageText: `You're receiving this message because you previous registered for the
<a href="https://editor.p5js.org">p5.js Web Editor</a>
using the same email address multiple times. In order to fix bugs and prevent future bugs,
your accounts have been consolidated to the first account you created. You can login with
the following email and username:`,
username: data.body.username,
email: data.body.email,
message2Text: `All of your sketches and collections have been preserved and have not been modified.
If you have forgotten your password you can reset it:`,
resetPasswordLink: 'reset-password',
resetPasswordText: 'Reset Password',
directLinkText: 'Or copy and paste the following URL into your browser:',
noteText: `We are grateful for your patience and understanding. Thank you for supporting p5.js and the
p5.js Web Editor!`,
meta: {
keywords: 'p5.js, p5.js web editor, web editor, processing, code editor',
description: 'A web editor for p5.js, a JavaScript library with the goal'
+ ' of making coding accessible to artists, designers, educators, and beginners.'
}
};
// Return MJML string
const template = consolidationMailLayout(templateOptions);
// Render MJML to HTML string
const html = renderMjml(template);
// Return options to send mail
return Object.assign(
{},
data,
{ html, subject },
);
};
export const renderResetPassword = (data) => {
const subject = 'p5.js Web Editor Password Reset';