diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 40e240ce26..7d3cc27f3f 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -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' }, }, diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 6b9587182c..db5039c0e6 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -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( @@ -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); }); } @@ -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, @@ -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 ); } diff --git a/src/RestWrite.js b/src/RestWrite.js index 315402cb00..5e1a3b00c8 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -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 @@ -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); + } } }); }; diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index b7c02fa2f5..159bb64117 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -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); @@ -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() { @@ -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); + }); } }