Skip to content

Changing user's email: keep using old email before verification new one #5147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Controllers/SchemaController.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const defaultColumns: { [string]: SchemaFields } = Object.freeze({
username: { type: 'String' },
password: { type: 'String' },
email: { type: 'String' },
emailNew: { type: 'String' },
emailVerified: { type: 'Boolean' },
authData: { type: 'Object' },
},
Expand Down
45 changes: 33 additions & 12 deletions src/Controllers/UserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ export class UserController extends AdaptableController {
return this.options.verifyUserEmails;
}

setEmailVerifyToken(user) {
setEmailVerifyToken(user, verified) {
if (this.shouldVerifyEmails) {
user._email_verify_token = randomString(25);
user.emailVerified = false;
if (!verified)
user.emailVerified = false;

if (this.config.emailVerifyTokenValidityDuration) {
user._email_verify_token_expires_at = Parse._encode(
Expand All @@ -58,23 +59,34 @@ export class UserController extends AdaptableController {
// if the email verify token needs to be validated then
// add additional query params and additional fields that need to be updated
if (this.config.emailVerifyTokenValidityDuration) {
query.emailVerified = false;
//query.emailVerified = false;
query._email_verify_token_expires_at = { $gt: Parse._encode(new Date()) };

updateFields._email_verify_token_expires_at = { __op: 'Delete' };
}
const masterAuth = Auth.master(this.config);
var checkIfAlreadyVerified = new RestQuery(
const getUser = new RestQuery(
this.config,
Auth.master(this.config),
'_User',
{ username: username, emailVerified: true }
query
);
return checkIfAlreadyVerified.execute().then(result => {
if (result.results.length) {
return Promise.resolve(result.results.length[0]);
return getUser.execute().then(result => {
if (!result.results.length) {
throw undefined;
}

const user = result.results[0];
if (user.emailVerified) {
if (!user.emailNew) {
return Promise.resolve(result.results.length[0]);
}

updateFields.email = user.emailNew;
updateFields.emailNew = '';
}
return rest.update(this.config, masterAuth, '_User', query, updateFields);

return rest.update(this.config, masterAuth, '_User', {username: username}, updateFields);
});
}

Expand Down Expand Up @@ -144,6 +156,9 @@ export class UserController extends AdaptableController {
this.getUserIfNeeded(user).then(user => {
const username = encodeURIComponent(user.username);

if (user.emailNew)
user = {...user, email: user.emailNew};

const link = buildEmailLink(
this.config.verifyEmailURL,
username,
Expand All @@ -169,12 +184,18 @@ export class UserController extends AdaptableController {
* @param user
* @returns {*}
*/
regenerateEmailVerifyToken(user) {
this.setEmailVerifyToken(user);
regenerateEmailVerifyToken(user, verified) {
this.setEmailVerifyToken(user, verified);
const updatedData = {
_email_verify_token: user._email_verify_token,
emailVerified: user.emailVerified,
};
if (user._email_verify_token_expires_at)
updatedData._email_verify_token_expires_at = user._email_verify_token_expires_at;
return this.config.database.update(
'_User',
{ username: user.username },
user
updatedData
);
}

Expand Down
16 changes: 13 additions & 3 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,14 @@ RestWrite.prototype.transformUser = function() {
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
}

// TODO: block manually email changing
/*
if (!this.auth.isMaster && 'email' in this.data && !('createdAt' in this.data)) {
const error = `Clients aren't allowed to manually update email. Please, use "requestEmailChange" function`;
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
}
*/

// Do not cleanup session if objectId is not set
if (this.query && this.objectId()) {
// If we're updating a _User object, we need to clear out the cache for that user. Find all their
Expand Down Expand Up @@ -595,9 +603,11 @@ RestWrite.prototype._validateEmail = function() {
(Object.keys(this.data.authData).length === 1 &&
Object.keys(this.data.authData)[0] === 'anonymous')
) {
// We updated the email, send a new validation
this.storage['sendVerificationEmail'] = true;
this.config.userController.setEmailVerifyToken(this.data);
if (!this.data.emailVerified) {
// We updated the email, send a new validation
this.storage['sendVerificationEmail'] = true;
this.config.userController.setEmailVerifyToken(this.data);
}
}
});
};
Expand Down
119 changes: 103 additions & 16 deletions src/Routers/UsersRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,80 @@ export class UsersRouter extends ClassesRouter {
);
}

handleEmailChange(req) {
const { email } = req.body;
let user;

if (!req.info || !req.info.sessionToken) {
throw new Parse.Error(
Parse.Error.INVALID_SESSION_TOKEN,
'Invalid session token'
);
}
const sessionToken = req.info.sessionToken;

return rest
.find(
req.config,
Auth.master(req.config),
'_Session',
{ sessionToken },
{ include: 'user' },
req.info.clientSDK
)
.then(response => {
if (
!response.results ||
response.results.length === 0 ||
!response.results[0].user
) {
throw new Parse.Error(
Parse.Error.INVALID_SESSION_TOKEN,
'Invalid session token'
);
}

user = response.results[0].user;
// Send token back on the login, because SDKs expect that.
user.sessionToken = sessionToken;

if (!email || typeof email !== 'string' || !email.match(/^.+@.+$/)) {
throw new Parse.Error(
Parse.Error.INVALID_EMAIL_ADDRESS,
'New email address is invalid.'
);
}

return req.config.database.find('_User', { email: email });
})

.then(results => {
if (results.length) {
throw new Parse.Error(
Parse.Error.EMAIL_TAKEN,
'Account already exists for new email address.'
);
}

user.emailNew = email;
return req.config.database.update(
'_User',
{ username: user.username },
{ emailNew: email});
})

.then(() => req.config.userController.regenerateEmailVerifyToken(user, true))

.then(() => {
req.config.userController.sendVerificationEmail(user);
return { response: {} };
})

.catch(error => {
throw error;
});
}

handleVerificationEmailRequest(req) {
this._throwOnBadEmailConfig(req);

Expand All @@ -387,30 +461,40 @@ export class UsersRouter extends ClassesRouter {
}

return req.config.database.find('_User', { email: email }).then(results => {
if (!results.length || results.length < 1) {
if (results.length) {
return results;
}

return req.config.database.find('_User', { emailNew: email }).then(results => {
if (results.length) {
return results;
}

throw new Parse.Error(
Parse.Error.EMAIL_NOT_FOUND,
`No user found with email ${email}`
);
}
const user = results[0];
});
})
.then(results => {
const user = results[0];

// remove password field, messes with saving on postgres
delete user.password;
// remove password field, messes with saving on postgres
delete user.password;

if (user.emailVerified) {
throw new Parse.Error(
Parse.Error.OTHER_CAUSE,
`Email ${email} is already verified.`
);
}
if (user.emailVerified && !user.emailNew) {
throw new Parse.Error(
Parse.Error.OTHER_CAUSE,
`Email ${email} is already verified.`
);
}

const userController = req.config.userController;
return userController.regenerateEmailVerifyToken(user).then(() => {
userController.sendVerificationEmail(user);
return { response: {} };
const userController = req.config.userController;
return userController.regenerateEmailVerifyToken(user, !!user.emailNew).then(() => {
userController.sendVerificationEmail(user);
return { response: {} };
});
});
});
}

mountRoutes() {
Expand Down Expand Up @@ -450,6 +534,9 @@ export class UsersRouter extends ClassesRouter {
this.route('GET', '/verifyPassword', req => {
return this.handleVerifyPassword(req);
});
this.route('POST', '/requestEmailChange', req => {
return this.handleEmailChange(req);
});
}
}

Expand Down