From 39c0ada49dbb5f079ba527ba5620054d840648aa Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 5 Feb 2023 18:11:32 +1100 Subject: [PATCH 01/13] feat: add conditional email --- spec/EmailVerificationToken.spec.js | 106 ++++++++++++++++++++++++++++ src/RestWrite.js | 28 ++++++-- src/triggers.js | 9 ++- 3 files changed, 137 insertions(+), 6 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index e21a049719..c592076e6e 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -288,6 +288,112 @@ describe('Email Verification Token Expiration: ', () => { }); }); + it('can conditionally send emails', async () => { + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', + }); + const beforeSave = { + method(req) { + expect(req.setSendEmailVerificationEmail).toBeTrue(); + expect(req.setEmailVerified).toBeFalse(); + req.setSendEmailVerificationEmail = false; + req.setEmailVerified = true; + }, + }; + const saveSpy = spyOn(beforeSave, 'method').and.callThrough(); + const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough(); + Parse.Cloud.beforeSave(Parse.User, beforeSave.method); + const user = new Parse.User(); + user.setUsername('sets_email_verify_token_expires_at'); + user.setPassword('expiringToken'); + user.set('email', 'user@example.com'); + await user.signUp(); + + const config = Config.get('test'); + const results = await config.database.find( + '_User', + { + username: 'sets_email_verify_token_expires_at', + }, + {}, + Auth.maintenance(config) + ); + + expect(results.length).toBe(1); + const user_data = results[0]; + expect(typeof user_data).toBe('object'); + expect(user_data.emailVerified).toEqual(true); + expect(user_data._email_verify_token).toBeUndefined(); + expect(user_data._email_verify_token_expires_at).toBeUndefined(); + expect(emailSpy).not.toHaveBeenCalled(); + expect(saveSpy).toHaveBeenCalled(); + expect(sendEmailOptions).toBeUndefined(); + }); + + it('beforeSave options do not change existing behaviour', async () => { + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', + }); + const beforeSave = { + method(req) { + if (!req.original) { + expect(req.setSendEmailVerificationEmail).toBeTrue(); + expect(req.setEmailVerified).toBeFalse(); + } + }, + }; + const saveSpy = spyOn(beforeSave, 'method').and.callThrough(); + const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough(); + Parse.Cloud.beforeSave(Parse.User, beforeSave.method); + const newUser = new Parse.User(); + newUser.setUsername('unsets_email_verify_token_expires_at'); + newUser.setPassword('expiringToken'); + newUser.set('email', 'user@parse.com'); + await newUser.signUp(); + const response = await request({ + url: sendEmailOptions.link, + followRedirects: false, + }); + expect(response.status).toEqual(302); + const config = Config.get('test'); + const results = await config.database.find('_User', { + username: 'unsets_email_verify_token_expires_at', + }); + + expect(results.length).toBe(1); + const user = results[0]; + expect(typeof user).toBe('object'); + expect(user.emailVerified).toEqual(true); + expect(typeof user._email_verify_token).toBe('undefined'); + expect(typeof user._email_verify_token_expires_at).toBe('undefined'); + expect(saveSpy).toHaveBeenCalled(); + expect(emailSpy).toHaveBeenCalled(); + }); + it('unsets the _email_verify_token_expires_at and _email_verify_token fields in the User class if email verification is successful', done => { const user = new Parse.User(); let sendEmailOptions; diff --git a/src/RestWrite.js b/src/RestWrite.js index 3a8385e52a..e50ae19ec3 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -240,6 +240,11 @@ RestWrite.prototype.runBeforeSaveTrigger = function () { identifier, }; + const additionalData = {}; + if (this.config.userController.shouldVerifyEmails && !originalObject) { + additionalData.setSendEmailVerificationEmail = true; + additionalData.setEmailVerified = false; + } return Promise.resolve() .then(() => { // Before calling the trigger, validate the permissions for the save operation @@ -277,7 +282,8 @@ RestWrite.prototype.runBeforeSaveTrigger = function () { updatedObject, originalObject, this.config, - this.context + this.context, + additionalData ); }) .then(response => { @@ -299,6 +305,13 @@ RestWrite.prototype.runBeforeSaveTrigger = function () { } } this.checkProhibitedKeywords(this.data); + + if (!additionalData.setSendEmailVerificationEmail) { + this.storage.sendVerificationEmail = false; + } + if (additionalData.setEmailVerified) { + this.storage.emailVerified = true; + } }); }; @@ -612,6 +625,10 @@ RestWrite.prototype.transformUser = function () { throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); } + if (this.storage.emailVerified) { + this.data.emailVerified = true; + } + // 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 @@ -742,10 +759,11 @@ RestWrite.prototype._validateEmail = function () { ); } if ( - !this.data.authData || - !Object.keys(this.data.authData).length || - (Object.keys(this.data.authData).length === 1 && - Object.keys(this.data.authData)[0] === 'anonymous') + (!this.data.authData || + !Object.keys(this.data.authData).length || + (Object.keys(this.data.authData).length === 1 && + Object.keys(this.data.authData)[0] === 'anonymous')) && + this.storage.sendVerificationEmail !== false ) { // We updated the email, send a new validation this.storage['sendVerificationEmail'] = true; diff --git a/src/triggers.js b/src/triggers.js index b5f11435df..612a153ec9 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -831,7 +831,8 @@ export function maybeRunTrigger( parseObject, originalParseObject, config, - context + context, + additionalData ) { if (!parseObject) { return Promise.resolve({}); @@ -847,6 +848,9 @@ export function maybeRunTrigger( config, context ); + if (additionalData) { + Object.assign(request, additionalData); + } var { success, error } = getResponseObject( request, object => { @@ -868,6 +872,9 @@ export function maybeRunTrigger( ) { Object.assign(context, request.context); } + for (const key in additionalData || {}) { + additionalData[key] = request[key]; + } resolve(object); }, error => { From f399e5c4f4f9a84b61df6b0c0a427dbe9744fa91 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 1 Mar 2023 13:44:35 +1100 Subject: [PATCH 02/13] wip --- spec/EmailVerificationToken.spec.js | 107 ++++++++++++++++++++++++++-- src/Controllers/UserController.js | 19 +++-- src/RestWrite.js | 77 +++++++++++--------- src/triggers.js | 9 +-- 4 files changed, 160 insertions(+), 52 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index c592076e6e..89aa0986b2 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -297,19 +297,23 @@ describe('Email Verification Token Expiration: ', () => { sendPasswordResetEmail: () => Promise.resolve(), sendMail: () => {}, }; + const verifyUserEmails = { + method(req) { + expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip']); + return false; + }, + }; + const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough(); await reconfigureServer({ appName: 'emailVerifyToken', - verifyUserEmails: true, + verifyUserEmails: verifyUserEmails.method, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds publicServerURL: 'http://localhost:8378/1', }); const beforeSave = { method(req) { - expect(req.setSendEmailVerificationEmail).toBeTrue(); - expect(req.setEmailVerified).toBeFalse(); - req.setSendEmailVerificationEmail = false; - req.setEmailVerified = true; + req.object.set('emailVerified', true); }, }; const saveSpy = spyOn(beforeSave, 'method').and.callThrough(); @@ -340,6 +344,99 @@ describe('Email Verification Token Expiration: ', () => { expect(emailSpy).not.toHaveBeenCalled(); expect(saveSpy).toHaveBeenCalled(); expect(sendEmailOptions).toBeUndefined(); + expect(verifySpy).toHaveBeenCalled(); + }); + + it('can conditionally send emails and allow conditional login', async () => { + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + const verifyUserEmails = { + method(req) { + expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip']); + if (req.object.get('username') === 'no_email') { + return false; + } + return true; + }, + }; + const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough(); + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: verifyUserEmails.method, + preventLoginWithUnverifiedEmail: verifyUserEmails.method, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', + silent: false, + }); + const user = new Parse.User(); + user.setUsername('no_email'); + user.setPassword('expiringToken'); + user.set('email', 'user@example.com'); + await user.signUp(); + expect(sendEmailOptions).toBeUndefined(); + expect(user.getSessionToken()).toBeDefined(); + expect(verifySpy).toHaveBeenCalledTimes(2); + const user2 = new Parse.User(); + user2.setUsername('email'); + user2.setPassword('expiringToken'); + user2.set('email', 'user2@example.com'); + await user2.signUp(); + expect(user2.getSessionToken()).toBeUndefined(); + expect(sendEmailOptions).toBeDefined(); + expect(verifySpy).toHaveBeenCalledTimes(4); + }); + + fit('can conditionally send emails and allow conditional login', async () => { + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + const verifyUserEmails = { + method(req) { + console.log(req); + expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip']); + if (req.object.get('username') === 'no_email') { + return false; + } + return true; + }, + }; + const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough(); + await reconfigureServer({ + appName: 'emailVerifyToken', + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', + verifyUserEmails: verifyUserEmails.method, + preventLoginWithUnverifiedEmail: verifyUserEmails.method + }); + const user = new Parse.User(); + user.setUsername('no_email'); + user.setPassword('expiringToken'); + user.set('email', 'user@example.com'); + await user.signUp(); + expect(sendEmailOptions).toBeUndefined(); + expect(user.getSessionToken()).toBeDefined(); + expect(verifySpy).toHaveBeenCalledTimes(2); + const user2 = new Parse.User(); + user2.setUsername('email'); + user2.setPassword('expiringToken'); + user2.set('email', 'user2@example.com'); + await user2.signUp(); + expect(user2.getSessionToken()).toBeUndefined(); + expect(sendEmailOptions).toBeDefined(); + expect(verifySpy).toHaveBeenCalledTimes(4); }); it('beforeSave options do not change existing behaviour', async () => { diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 6871add987..cb1f143795 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -32,13 +32,24 @@ export class UserController extends AdaptableController { } get shouldVerifyEmails() { - return this.options.verifyUserEmails; + return (this.config || this.options).verifyUserEmails; } - setEmailVerifyToken(user) { - if (this.shouldVerifyEmails) { + async setEmailVerifyToken(user, req, storage) { + let shouldSendEmail = this.shouldVerifyEmails; + if (typeof shouldSendEmail === 'function') { + const response = await Promise.resolve(this.shouldVerifyEmails(req)); + shouldSendEmail = !!response; + } + if (shouldSendEmail) { + storage.sendVerificationEmail = true; user._email_verify_token = randomString(25); - user.emailVerified = false; + if ( + !storage.fieldsChangedByTrigger || + !storage.fieldsChangedByTrigger.includes('emailVerified') + ) { + user.emailVerified = false; + } if (this.config.emailVerifyTokenValidityDuration) { user._email_verify_token_expires_at = Parse._encode( diff --git a/src/RestWrite.js b/src/RestWrite.js index e50ae19ec3..ce86f18572 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -113,6 +113,9 @@ RestWrite.prototype.execute = function () { .then(() => { return this.validateAuthData(); }) + .then(() => { + return this.checkRestrictedFields(); + }) .then(() => { return this.runBeforeSaveTrigger(); }) @@ -240,11 +243,6 @@ RestWrite.prototype.runBeforeSaveTrigger = function () { identifier, }; - const additionalData = {}; - if (this.config.userController.shouldVerifyEmails && !originalObject) { - additionalData.setSendEmailVerificationEmail = true; - additionalData.setEmailVerified = false; - } return Promise.resolve() .then(() => { // Before calling the trigger, validate the permissions for the save operation @@ -282,8 +280,7 @@ RestWrite.prototype.runBeforeSaveTrigger = function () { updatedObject, originalObject, this.config, - this.context, - additionalData + this.context ); }) .then(response => { @@ -305,13 +302,6 @@ RestWrite.prototype.runBeforeSaveTrigger = function () { } } this.checkProhibitedKeywords(this.data); - - if (!additionalData.setSendEmailVerificationEmail) { - this.storage.sendVerificationEmail = false; - } - if (additionalData.setEmailVerified) { - this.storage.emailVerified = true; - } }); }; @@ -613,20 +603,22 @@ RestWrite.prototype.handleAuthData = async function (authData) { } }; -// The non-third-party parts of User transformation -RestWrite.prototype.transformUser = function () { - var promise = Promise.resolve(); +RestWrite.prototype.checkRestrictedFields = async function () { if (this.className !== '_User') { - return promise; + return; } if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) { const error = `Clients aren't allowed to manually update email verification.`; throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); } +}; - if (this.storage.emailVerified) { - this.data.emailVerified = true; +// The non-third-party parts of User transformation +RestWrite.prototype.transformUser = function () { + var promise = Promise.resolve(); + if (this.className !== '_User') { + return promise; } // Do not cleanup session if objectId is not set @@ -759,15 +751,20 @@ RestWrite.prototype._validateEmail = function () { ); } if ( - (!this.data.authData || - !Object.keys(this.data.authData).length || - (Object.keys(this.data.authData).length === 1 && - Object.keys(this.data.authData)[0] === 'anonymous')) && - this.storage.sendVerificationEmail !== false + !this.data.authData || + !Object.keys(this.data.authData).length || + (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); + const { originalObject, updatedObject } = this.buildParseObjects(); + const request = { + original: originalObject, + object: updatedObject, + master: this.auth.isMaster, + ip: this.config.ip, + }; + return this.config.userController.setEmailVerifyToken(this.data, request, this.storage); } }); }; @@ -879,7 +876,7 @@ RestWrite.prototype._validatePasswordHistory = function () { return Promise.resolve(); }; -RestWrite.prototype.createSessionTokenIfNeeded = function () { +RestWrite.prototype.createSessionTokenIfNeeded = async function () { if (this.className !== '_User') { return; } @@ -891,13 +888,23 @@ RestWrite.prototype.createSessionTokenIfNeeded = function () { if (this.auth.user && this.data.authData) { return; } - if ( - !this.storage.authProvider && // signup call, with - this.config.preventLoginWithUnverifiedEmail && // no login without verification - this.config.verifyUserEmails - ) { - // verification is on - return; // do not create the session token in that case! + if (!this.storage.authProvider && this.config.verifyUserEmails) { + let shouldPreventUnverifedLogin = this.config.preventLoginWithUnverifiedEmail; + if (typeof this.config.preventLoginWithUnverifiedEmail === 'function') { + const { originalObject, updatedObject } = this.buildParseObjects(); + const request = { + original: originalObject, + object: updatedObject, + master: this.auth.isMaster, + ip: this.config.ip, + }; + shouldPreventUnverifedLogin = await Promise.resolve( + this.config.preventLoginWithUnverifiedEmail(request) + ); + } + if (shouldPreventUnverifedLogin) { + return; + } } return this.createSessionToken(); }; diff --git a/src/triggers.js b/src/triggers.js index 612a153ec9..b5f11435df 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -831,8 +831,7 @@ export function maybeRunTrigger( parseObject, originalParseObject, config, - context, - additionalData + context ) { if (!parseObject) { return Promise.resolve({}); @@ -848,9 +847,6 @@ export function maybeRunTrigger( config, context ); - if (additionalData) { - Object.assign(request, additionalData); - } var { success, error } = getResponseObject( request, object => { @@ -872,9 +868,6 @@ export function maybeRunTrigger( ) { Object.assign(context, request.context); } - for (const key in additionalData || {}) { - additionalData[key] = request[key]; - } resolve(object); }, error => { From edd34952322a6d3c3ee27ffc4bd33c15b3f9721b Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 2 Mar 2023 13:10:34 +1100 Subject: [PATCH 03/13] Update EmailVerificationToken.spec.js --- spec/EmailVerificationToken.spec.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 89aa0986b2..4b15646429 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -393,7 +393,7 @@ describe('Email Verification Token Expiration: ', () => { expect(verifySpy).toHaveBeenCalledTimes(4); }); - fit('can conditionally send emails and allow conditional login', async () => { + it('can conditionally send emails and allow conditional login', async () => { let sendEmailOptions; const emailAdapter = { sendVerificationEmail: options => { @@ -404,7 +404,6 @@ describe('Email Verification Token Expiration: ', () => { }; const verifyUserEmails = { method(req) { - console.log(req); expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip']); if (req.object.get('username') === 'no_email') { return false; @@ -419,7 +418,7 @@ describe('Email Verification Token Expiration: ', () => { emailVerifyTokenValidityDuration: 5, // 5 seconds publicServerURL: 'http://localhost:8378/1', verifyUserEmails: verifyUserEmails.method, - preventLoginWithUnverifiedEmail: verifyUserEmails.method + preventLoginWithUnverifiedEmail: verifyUserEmails.method, }); const user = new Parse.User(); user.setUsername('no_email'); From ce9f3f574db679836e3e29dc05144dce6902fe75 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 2 Mar 2023 13:16:04 +1100 Subject: [PATCH 04/13] refactor --- src/Controllers/UserController.js | 2 +- src/RestWrite.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index cb1f143795..e5ad158797 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -39,7 +39,7 @@ export class UserController extends AdaptableController { let shouldSendEmail = this.shouldVerifyEmails; if (typeof shouldSendEmail === 'function') { const response = await Promise.resolve(this.shouldVerifyEmails(req)); - shouldSendEmail = !!response; + shouldSendEmail = response !== false; } if (shouldSendEmail) { storage.sendVerificationEmail = true; diff --git a/src/RestWrite.js b/src/RestWrite.js index ce86f18572..0e38f63e21 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -902,7 +902,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () { this.config.preventLoginWithUnverifiedEmail(request) ); } - if (shouldPreventUnverifedLogin) { + if (shouldPreventUnverifedLogin === true) { return; } } From 95785e27a4812f2bd10dd512acb23fd2de0bee11 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 29 Mar 2023 12:21:39 +1100 Subject: [PATCH 05/13] add sendUserEmailVerification --- spec/EmailVerificationToken.spec.js | 44 +++++++--- spec/MongoStorageAdapter.spec.js | 2 +- spec/ParseLiveQueryServer.spec.js | 3 +- spec/ValidationAndPasswordsReset.spec.js | 103 ++++++++++++----------- src/Controllers/UserController.js | 103 +++++++++++++---------- src/Options/Definitions.js | 9 +- src/Options/docs.js | 3 +- src/Options/index.js | 8 +- src/RestWrite.js | 2 +- src/Routers/PagesRouter.js | 2 +- src/Routers/PublicAPIRouter.js | 2 +- src/Routers/UsersRouter.js | 34 ++++---- 12 files changed, 185 insertions(+), 130 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 4b15646429..2022713906 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -438,6 +438,39 @@ describe('Email Verification Token Expiration: ', () => { expect(verifySpy).toHaveBeenCalledTimes(4); }); + it('can conditionally send user email verification', async () => { + const emailAdapter = { + sendVerificationEmail: () => {}, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + const sendVerificationEmail = { + method(req) { + expect(req.user).toBeDefined(); + expect(req.master).toBeDefined(); + return false; + }, + }; + const sendSpy = spyOn(sendVerificationEmail, 'method').and.callThrough(); + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', + sendUserEmailVerification: sendVerificationEmail.method, + }); + const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough(); + const newUser = new Parse.User(); + newUser.setUsername('unsets_email_verify_token_expires_at'); + newUser.setPassword('expiringToken'); + newUser.set('email', 'user@example.com'); + await newUser.signUp(); + await Parse.User.requestEmailVerification('user@example.com'); + expect(sendSpy).toHaveBeenCalledTimes(2); + expect(emailSpy).toHaveBeenCalledTimes(0); + }); + it('beforeSave options do not change existing behaviour', async () => { let sendEmailOptions; const emailAdapter = { @@ -454,17 +487,7 @@ describe('Email Verification Token Expiration: ', () => { emailVerifyTokenValidityDuration: 5, // 5 seconds publicServerURL: 'http://localhost:8378/1', }); - const beforeSave = { - method(req) { - if (!req.original) { - expect(req.setSendEmailVerificationEmail).toBeTrue(); - expect(req.setEmailVerified).toBeFalse(); - } - }, - }; - const saveSpy = spyOn(beforeSave, 'method').and.callThrough(); const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough(); - Parse.Cloud.beforeSave(Parse.User, beforeSave.method); const newUser = new Parse.User(); newUser.setUsername('unsets_email_verify_token_expires_at'); newUser.setPassword('expiringToken'); @@ -486,7 +509,6 @@ describe('Email Verification Token Expiration: ', () => { expect(user.emailVerified).toEqual(true); expect(typeof user._email_verify_token).toBe('undefined'); expect(typeof user._email_verify_token_expires_at).toBe('undefined'); - expect(saveSpy).toHaveBeenCalled(); expect(emailSpy).toHaveBeenCalled(); }); diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index 06fdab4fca..1b5cc0c5e9 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -248,7 +248,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { expect(object.date[0] instanceof Date).toBeTrue(); expect(object.bar.date[0] instanceof Date).toBeTrue(); expect(object.foo.test.date[0] instanceof Date).toBeTrue(); - const obj = await new Parse.Query('MyClass').first({useMasterKey: true}); + const obj = await new Parse.Query('MyClass').first({ useMasterKey: true }); expect(obj.get('date')[0] instanceof Date).toBeTrue(); expect(obj.get('bar').date[0] instanceof Date).toBeTrue(); expect(obj.get('foo').test.date[0] instanceof Date).toBeTrue(); diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index 5b4690ae85..a018a71afa 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -1138,8 +1138,7 @@ describe('ParseLiveQueryServer', function () { expect(toSend.original).toBeUndefined(); expect(spy).toHaveBeenCalledWith({ usage: 'Subscribing using fields parameter', - solution: - `Subscribe using "keys" instead.`, + solution: `Subscribe using "keys" instead.`, }); }); diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 3272f07fc3..b9ce176c18 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -242,8 +242,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); - it('allows user to login only after user clicks on the link to confirm email address if preventLoginWithUnverifiedEmail is set to true', done => { - const user = new Parse.User(); + it('allows user to login only after user clicks on the link to confirm email address if preventLoginWithUnverifiedEmail is set to true', async () => { let sendEmailOptions; const emailAdapter = { sendVerificationEmail: options => { @@ -252,59 +251,34 @@ describe('Custom Pages, Email Verification, Password Reset', () => { sendPasswordResetEmail: () => Promise.resolve(), sendMail: () => {}, }; - reconfigureServer({ + await reconfigureServer({ appName: 'emailing app', verifyUserEmails: true, preventLoginWithUnverifiedEmail: true, emailAdapter: emailAdapter, publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - user.setPassword('other-password'); - user.setUsername('user'); - user.set('email', 'user@parse.com'); - return user.signUp(); - }) - .then(() => { - expect(sendEmailOptions).not.toBeUndefined(); - request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' - ); - user - .fetch({ useMasterKey: true }) - .then( - () => { - expect(user.get('emailVerified')).toEqual(true); - - Parse.User.logIn('user', 'other-password').then( - user => { - expect(typeof user).toBe('object'); - expect(user.get('emailVerified')).toBe(true); - done(); - }, - () => { - fail('login should have succeeded'); - done(); - } - ); - }, - err => { - jfail(err); - fail('this should not fail'); - done(); - } - ) - .catch(err => { - jfail(err); - done(); - }); - }); - }); + }); + let user = new Parse.User(); + user.setPassword('other-password'); + user.setUsername('user'); + user.set('email', 'user@example.com'); + await expectAsync(user.signUp()).toBeRejectedWith( + new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.') + ); + expect(sendEmailOptions).not.toBeUndefined(); + const response = await request({ + url: sendEmailOptions.link, + followRedirects: false, + }); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' + ); + user = await new Parse.Query(Parse.User).first({ useMasterKey: true }); + expect(user.get('emailVerified')).toEqual(true); + user = await Parse.User.logIn('user', 'other-password'); + expect(typeof user).toBe('object'); + expect(user.get('emailVerified')).toBe(true); }); it('allows user to login if email is not verified but preventLoginWithUnverifiedEmail is set to false', done => { @@ -345,6 +319,35 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); + it('does not allow signup with preventSignupWithUnverified', async () => { + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + await reconfigureServer({ + appName: 'test', + publicServerURL: 'http://localhost:1337/1', + verifyUserEmails: true, + preventLoginWithUnverifiedEmail: true, + preventSignupWithUnverifiedEmail: true, + emailAdapter, + }); + const newUser = new Parse.User(); + newUser.setPassword('asdf'); + newUser.setUsername('zxcv'); + newUser.set('email', 'test@example.com'); + await expectAsync(newUser.signUp()).toBeRejectedWith( + new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.') + ); + const user = await new Parse.Query(Parse.User).first({ useMasterKey: true }); + expect(user).toBeDefined(); + expect(sendEmailOptions).toBeDefined(); + }); + it('fails if you include an emailAdapter, set a publicServerURL, but have no appName and send a password reset email', done => { reconfigureServer({ appName: undefined, diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index e5ad158797..1e2881056f 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -35,28 +35,30 @@ export class UserController extends AdaptableController { return (this.config || this.options).verifyUserEmails; } - async setEmailVerifyToken(user, req, storage) { + async setEmailVerifyToken(user, req, storage = {}) { let shouldSendEmail = this.shouldVerifyEmails; if (typeof shouldSendEmail === 'function') { - const response = await Promise.resolve(this.shouldVerifyEmails(req)); + const response = await Promise.resolve(shouldSendEmail(req)); shouldSendEmail = response !== false; } - if (shouldSendEmail) { - storage.sendVerificationEmail = true; - user._email_verify_token = randomString(25); - if ( - !storage.fieldsChangedByTrigger || - !storage.fieldsChangedByTrigger.includes('emailVerified') - ) { - user.emailVerified = false; - } + if (!shouldSendEmail) { + return false; + } + storage.sendVerificationEmail = true; + user._email_verify_token = randomString(25); + if ( + !storage.fieldsChangedByTrigger || + !storage.fieldsChangedByTrigger.includes('emailVerified') + ) { + user.emailVerified = false; + } - if (this.config.emailVerifyTokenValidityDuration) { - user._email_verify_token_expires_at = Parse._encode( - this.config.generateEmailVerifyTokenExpiresAt() - ); - } + if (this.config.emailVerifyTokenValidityDuration) { + user._email_verify_token_expires_at = Parse._encode( + this.config.generateEmailVerifyTokenExpiresAt() + ); } + return true; } verifyEmail(username, token) { @@ -142,27 +144,39 @@ export class UserController extends AdaptableController { }); } - sendVerificationEmail(user) { + async sendVerificationEmail(user, req) { if (!this.shouldVerifyEmails) { return; } const token = encodeURIComponent(user._email_verify_token); // We may need to fetch the user in case of update email - this.getUserIfNeeded(user).then(user => { - const username = encodeURIComponent(user.username); - - const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config); - const options = { - appName: this.config.appName, - link: link, - user: inflate('_User', user), - }; - if (this.adapter.sendVerificationEmail) { - this.adapter.sendVerificationEmail(options); - } else { - this.adapter.sendMail(this.defaultVerificationEmail(options)); - } - }); + const fetchedUser = await this.getUserIfNeeded(user); + let shouldSendEmail = this.config.sendUserEmailVerification; + if (typeof shouldSendEmail === 'function') { + const response = await Promise.resolve( + this.config.sendUserEmailVerification({ + user: Parse.Object.fromJSON({ className: '_User', ...fetchedUser }), + master: req.auth?.isMaster, + }) + ); + shouldSendEmail = !!response; + } + if (!shouldSendEmail) { + return; + } + const username = encodeURIComponent(user.username); + + const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config); + const options = { + appName: this.config.appName, + link: link, + user: inflate('_User', fetchedUser), + }; + if (this.adapter.sendVerificationEmail) { + this.adapter.sendVerificationEmail(options); + } else { + this.adapter.sendMail(this.defaultVerificationEmail(options)); + } } /** @@ -171,7 +185,7 @@ export class UserController extends AdaptableController { * @param user * @returns {*} */ - regenerateEmailVerifyToken(user) { + async regenerateEmailVerifyToken(user, master) { const { _email_verify_token } = user; let { _email_verify_token_expires_at } = user; if (_email_verify_token_expires_at && _email_verify_token_expires_at.__type === 'Date') { @@ -185,19 +199,22 @@ export class UserController extends AdaptableController { ) { return Promise.resolve(); } - this.setEmailVerifyToken(user); + const shouldSend = await this.setEmailVerifyToken(user, { user, master }); + if (!shouldSend) { + return; + } return this.config.database.update('_User', { username: user.username }, user); } - resendVerificationEmail(username) { - return this.getUserIfNeeded({ username: username }).then(aUser => { - if (!aUser || aUser.emailVerified) { - throw undefined; - } - return this.regenerateEmailVerifyToken(aUser).then(() => { - this.sendVerificationEmail(aUser); - }); - }); + async resendVerificationEmail(username, req) { + const aUser = await this.getUserIfNeeded({ username: username }); + if (!aUser || aUser.emailVerified) { + throw undefined; + } + const generate = await this.regenerateEmailVerifyToken(aUser, req.auth.isMaster); + if (generate) { + this.sendVerificationEmail(aUser, req); + } } setPasswordResetToken(email) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index a25e69c70a..f1f7d44cf7 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -481,6 +481,13 @@ module.exports.ParseServerOptions = { action: parsers.objectParser, default: {}, }, + sendUserEmailVerification: { + env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION', + help: + 'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for condiitonal email sending.

Default is `true`.
', + action: parsers.booleanParser, + default: true, + }, serverCloseComplete: { env: 'PARSE_SERVER_SERVER_CLOSE_COMPLETE', help: 'Callback when server has closed', @@ -527,7 +534,7 @@ module.exports.ParseServerOptions = { verifyUserEmails: { env: 'PARSE_SERVER_VERIFY_USER_EMAILS', help: - 'Set to `true` to require users to verify their email address to complete the sign-up process.

Default is `false`.', + 'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for condiitonal verification.

Default is `false`.', action: parsers.booleanParser, default: false, }, diff --git a/src/Options/docs.js b/src/Options/docs.js index 0eb6488c74..abebc9ac81 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -87,6 +87,7 @@ * @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false. * @property {SchemaOptions} schema Defined schema * @property {SecurityOptions} security The security options to identify and report weak security settings. + * @property {Boolean} sendUserEmailVerification Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for condiitonal email sending.

Default is `true`.
* @property {Function} serverCloseComplete Callback when server has closed * @property {String} serverURL URL to your parse server with http:// or https://. * @property {Number} sessionLength Session duration, in seconds, defaults to 1 year @@ -95,7 +96,7 @@ * @property {Any} trustProxy The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the express trust proxy settings documentation. Defaults to `false`. * @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields * @property {Boolean} verbose Set the logging to verbose - * @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process.

Default is `false`. + * @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for condiitonal verification.

Default is `false`. * @property {String} webhookKey Key sent with outgoing webhook calls */ diff --git a/src/Options/index.js b/src/Options/index.js index 778374e7e7..fdb3cca270 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -152,7 +152,7 @@ export interface ParseServerOptions { /* Max file size for uploads, defaults to 20mb :DEFAULT: 20mb */ maxUploadSize: ?string; - /* Set to `true` to require users to verify their email address to complete the sign-up process. + /* Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for condiitonal verification.

Default is `false`. :DEFAULT: false */ @@ -180,6 +180,12 @@ export interface ParseServerOptions { Requires option `verifyUserEmails: true`. :DEFAULT: false */ emailVerifyTokenReuseIfValid: ?boolean; + /* Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for condiitonal email sending. +

+ Default is `true`. +
+ :DEFAULT: true */ + sendUserEmailVerification: ?boolean; /* The account lockout policy for failed login attempts. */ accountLockout: ?AccountLockoutOptions; /* The password policy for enforcing password related rules. */ diff --git a/src/RestWrite.js b/src/RestWrite.js index 0e38f63e21..067f3a2a01 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1031,7 +1031,7 @@ RestWrite.prototype.handleFollowup = function () { if (this.storage && this.storage['sendVerificationEmail']) { delete this.storage['sendVerificationEmail']; // Fire and forget! - this.config.userController.sendVerificationEmail(this.data); + this.config.userController.sendVerificationEmail(this.data, { auth: this.auth }); return this.handleFollowup.bind(this); } }; diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 5d5a1467a7..79a487b6e4 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -125,7 +125,7 @@ export class PagesRouter extends PromiseRouter { const userController = config.userController; - return userController.resendVerificationEmail(username).then( + return userController.resendVerificationEmail(username, req).then( () => { return this.goToPage(req, pages.emailVerificationSendSuccess); }, diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 5009ee7d22..ddef76a5b8 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -63,7 +63,7 @@ export class PublicAPIRouter extends PromiseRouter { const userController = config.userController; - return userController.resendVerificationEmail(username).then( + return userController.resendVerificationEmail(username, req).then( () => { return Promise.resolve({ status: 302, diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 4a72fdd73b..4d528a23e9 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -446,7 +446,7 @@ export class UsersRouter extends ClassesRouter { } } - handleVerificationEmailRequest(req) { + async handleVerificationEmailRequest(req) { this._throwOnBadEmailConfig(req); const { email } = req.body; @@ -460,25 +460,25 @@ export class UsersRouter extends ClassesRouter { ); } - return req.config.database.find('_User', { email: email }).then(results => { - if (!results.length || results.length < 1) { - throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`); - } - const user = results[0]; + const results = await req.config.database.find('_User', { email: email }); + if (!results.length || results.length < 1) { + throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`); + } + 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) { + 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; + const send = await userController.regenerateEmailVerifyToken(user, req.auth.isMaster); + if (send) { + userController.sendVerificationEmail(user, req); + } + return { response: {} }; } async handleChallenge(req) { From 006356ad5122008c8edaf01f1e2e182539798eb8 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 29 Mar 2023 13:13:03 +1100 Subject: [PATCH 06/13] tests --- spec/EmailVerificationToken.spec.js | 46 ------------------------ spec/ValidationAndPasswordsReset.spec.js | 33 +---------------- src/Controllers/UserController.js | 2 +- 3 files changed, 2 insertions(+), 79 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 2022713906..a7a59b893e 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -373,52 +373,6 @@ describe('Email Verification Token Expiration: ', () => { emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds publicServerURL: 'http://localhost:8378/1', - silent: false, - }); - const user = new Parse.User(); - user.setUsername('no_email'); - user.setPassword('expiringToken'); - user.set('email', 'user@example.com'); - await user.signUp(); - expect(sendEmailOptions).toBeUndefined(); - expect(user.getSessionToken()).toBeDefined(); - expect(verifySpy).toHaveBeenCalledTimes(2); - const user2 = new Parse.User(); - user2.setUsername('email'); - user2.setPassword('expiringToken'); - user2.set('email', 'user2@example.com'); - await user2.signUp(); - expect(user2.getSessionToken()).toBeUndefined(); - expect(sendEmailOptions).toBeDefined(); - expect(verifySpy).toHaveBeenCalledTimes(4); - }); - - it('can conditionally send emails and allow conditional login', async () => { - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - const verifyUserEmails = { - method(req) { - expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip']); - if (req.object.get('username') === 'no_email') { - return false; - } - return true; - }, - }; - const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough(); - await reconfigureServer({ - appName: 'emailVerifyToken', - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1', - verifyUserEmails: verifyUserEmails.method, - preventLoginWithUnverifiedEmail: verifyUserEmails.method, }); const user = new Parse.User(); user.setUsername('no_email'); diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index b9ce176c18..46c1bb5b72 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -262,9 +262,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { user.setPassword('other-password'); user.setUsername('user'); user.set('email', 'user@example.com'); - await expectAsync(user.signUp()).toBeRejectedWith( - new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.') - ); + await user.signUp(); expect(sendEmailOptions).not.toBeUndefined(); const response = await request({ url: sendEmailOptions.link, @@ -319,35 +317,6 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); - it('does not allow signup with preventSignupWithUnverified', async () => { - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - await reconfigureServer({ - appName: 'test', - publicServerURL: 'http://localhost:1337/1', - verifyUserEmails: true, - preventLoginWithUnverifiedEmail: true, - preventSignupWithUnverifiedEmail: true, - emailAdapter, - }); - const newUser = new Parse.User(); - newUser.setPassword('asdf'); - newUser.setUsername('zxcv'); - newUser.set('email', 'test@example.com'); - await expectAsync(newUser.signUp()).toBeRejectedWith( - new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.') - ); - const user = await new Parse.Query(Parse.User).first({ useMasterKey: true }); - expect(user).toBeDefined(); - expect(sendEmailOptions).toBeDefined(); - }); - it('fails if you include an emailAdapter, set a publicServerURL, but have no appName and send a password reset email', done => { reconfigureServer({ appName: undefined, diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 1e2881056f..7618f500bf 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -211,7 +211,7 @@ export class UserController extends AdaptableController { if (!aUser || aUser.emailVerified) { throw undefined; } - const generate = await this.regenerateEmailVerifyToken(aUser, req.auth.isMaster); + const generate = await this.regenerateEmailVerifyToken(aUser, req.auth?.isMaster); if (generate) { this.sendVerificationEmail(aUser, req); } From ed71cfdea48845eeb885318c03c457e497c5501d Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 29 Mar 2023 14:03:46 +1100 Subject: [PATCH 07/13] tests --- spec/UserController.spec.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 6bcc454baf..7b98367702 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -1,4 +1,3 @@ -const UserController = require('../lib/Controllers/UserController').UserController; const emailAdapter = require('./support/MockEmailAdapter'); describe('UserController', () => { @@ -11,11 +10,14 @@ describe('UserController', () => { describe('sendVerificationEmail', () => { describe('parseFrameURL not provided', () => { it('uses publicServerURL', async done => { - await reconfigureServer({ + const server = await reconfigureServer({ publicServerURL: 'http://www.example.com', customPages: { parseFrameURL: undefined, }, + verifyUserEmails: true, + emailAdapter, + appName: 'test', }); emailAdapter.sendVerificationEmail = options => { expect(options.link).toEqual( @@ -24,20 +26,20 @@ describe('UserController', () => { emailAdapter.sendVerificationEmail = () => Promise.resolve(); done(); }; - const userController = new UserController(emailAdapter, 'test', { - verifyUserEmails: true, - }); - userController.sendVerificationEmail(user); + server.config.userController.sendVerificationEmail(user); }); }); describe('parseFrameURL provided', () => { it('uses parseFrameURL and includes the destination in the link parameter', async done => { - await reconfigureServer({ + const server = await reconfigureServer({ publicServerURL: 'http://www.example.com', customPages: { parseFrameURL: 'http://someother.example.com/handle-parse-iframe', }, + verifyUserEmails: true, + emailAdapter, + appName: 'test', }); emailAdapter.sendVerificationEmail = options => { expect(options.link).toEqual( @@ -46,10 +48,7 @@ describe('UserController', () => { emailAdapter.sendVerificationEmail = () => Promise.resolve(); done(); }; - const userController = new UserController(emailAdapter, 'test', { - verifyUserEmails: true, - }); - userController.sendVerificationEmail(user); + server.config.userController.sendVerificationEmail(user); }); }); }); From bba6750d6bcbd4c5395c0bc537a588660e5a863c Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 21 May 2023 03:41:42 +0200 Subject: [PATCH 08/13] fix typo Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- src/Options/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Options/index.js b/src/Options/index.js index dcf7ecb65a..95b57a3419 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -153,7 +153,7 @@ export interface ParseServerOptions { /* Max file size for uploads, defaults to 20mb :DEFAULT: 20mb */ maxUploadSize: ?string; - /* Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for condiitonal verification. + /* Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.

Default is `false`. :DEFAULT: false */ From 5283b60ff55b7dcdd822a2cb99336bc52a7f2c01 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 21 May 2023 03:50:21 +0200 Subject: [PATCH 09/13] fix typo in definitions Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- src/Options/Definitions.js | 2 +- src/Options/docs.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index aa4219010c..996011b259 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -542,7 +542,7 @@ module.exports.ParseServerOptions = { verifyUserEmails: { env: 'PARSE_SERVER_VERIFY_USER_EMAILS', help: - 'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for condiitonal verification.

Default is `false`.', + 'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.

Default is `false`.', action: parsers.booleanParser, default: false, }, diff --git a/src/Options/docs.js b/src/Options/docs.js index 5597a6d887..510e0c4ac6 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -88,7 +88,7 @@ * @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false. * @property {SchemaOptions} schema Defined schema * @property {SecurityOptions} security The security options to identify and report weak security settings. - * @property {Boolean} sendUserEmailVerification Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for condiitonal email sending.

Default is `true`.
+ * @property {Boolean} sendUserEmailVerification Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.

Default is `true`.
* @property {Function} serverCloseComplete Callback when server has closed * @property {String} serverURL URL to your parse server with http:// or https://. * @property {Number} sessionLength Session duration, in seconds, defaults to 1 year From 4d4033946a2c776127602986a3072ab0bcda2f88 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 21 May 2023 03:54:32 +0200 Subject: [PATCH 10/13] fix typo --- src/Options/Definitions.js | 2 +- src/Options/docs.js | 2 +- src/Options/index.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 996011b259..f0ff5ac0e0 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -492,7 +492,7 @@ module.exports.ParseServerOptions = { sendUserEmailVerification: { env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION', help: - 'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for condiitonal email sending.

Default is `true`.
', + 'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.

Default is `true`.
', action: parsers.booleanParser, default: true, }, diff --git a/src/Options/docs.js b/src/Options/docs.js index 510e0c4ac6..389e5711e0 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -97,7 +97,7 @@ * @property {Any} trustProxy The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the express trust proxy settings documentation. Defaults to `false`. * @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields * @property {Boolean} verbose Set the logging to verbose - * @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for condiitonal verification.

Default is `false`. + * @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.

Default is `false`. * @property {String} webhookKey Key sent with outgoing webhook calls */ diff --git a/src/Options/index.js b/src/Options/index.js index 95b57a3419..07a8a561c2 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -181,7 +181,7 @@ export interface ParseServerOptions { Requires option `verifyUserEmails: true`. :DEFAULT: false */ emailVerifyTokenReuseIfValid: ?boolean; - /* Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for condiitonal email sending. + /* Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.

Default is `true`.
From 50314b39493382235a39d06dfc7c2e2a0596fd8b Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 22 May 2023 17:16:03 +1000 Subject: [PATCH 11/13] wip --- resources/buildConfigDefinitions.js | 11 ++++++++++- src/Options/Definitions.js | 2 -- src/Options/index.js | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index e0d33daa4b..0be6e0085d 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -255,7 +255,16 @@ function inject(t, list) { props.push(t.objectProperty(t.stringLiteral('action'), action)); } if (elt.defaultValue) { - const parsedValue = parseDefaultValue(elt, elt.defaultValue, t); + let parsedValue = parseDefaultValue(elt, elt.defaultValue, t); + if (!parsedValue) { + for (const type of elt.typeAnnotation.types) { + elt.type = type.type; + parsedValue = parseDefaultValue(elt, elt.defaultValue, t); + if (parsedValue) { + break; + } + } + } if (parsedValue) { props.push(t.objectProperty(t.stringLiteral('default'), parsedValue)); } else { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index f0ff5ac0e0..d55066cca5 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -493,7 +493,6 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION', help: 'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.

Default is `true`.
', - action: parsers.booleanParser, default: true, }, serverCloseComplete: { @@ -543,7 +542,6 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_VERIFY_USER_EMAILS', help: 'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.

Default is `false`.', - action: parsers.booleanParser, default: false, }, webhookKey: { diff --git a/src/Options/index.js b/src/Options/index.js index 07a8a561c2..9c28eedbcd 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -157,7 +157,7 @@ export interface ParseServerOptions {

Default is `false`. :DEFAULT: false */ - verifyUserEmails: ?boolean; + verifyUserEmails: ?(boolean | void); /* Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required.

Default is `false`. @@ -186,7 +186,7 @@ export interface ParseServerOptions { Default is `true`.
:DEFAULT: true */ - sendUserEmailVerification: ?boolean; + sendUserEmailVerification: ?(boolean | void); /* The account lockout policy for failed login attempts. */ accountLockout: ?AccountLockoutOptions; /* The password policy for enforcing password related rules. */ From d9eb91f3a736735a63b9b4f463dc0164cc89c3db Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 9 Jun 2023 17:45:43 +1000 Subject: [PATCH 12/13] tests --- src/RestWrite.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 11dd3f9eb5..836e3ee6d3 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -891,6 +891,15 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () { if (this.auth.user && this.data.authData) { return; } + if ( + !this.storage.authProvider && // signup call, with + this.config.preventLoginWithUnverifiedEmail && // no login without verification + this.config.verifyUserEmails + ) { + // verification is on + this.storage.rejectSignup = true; + return; + } if (!this.storage.authProvider && this.config.verifyUserEmails) { let shouldPreventUnverifedLogin = this.config.preventLoginWithUnverifiedEmail; if (typeof this.config.preventLoginWithUnverifiedEmail === 'function') { @@ -909,15 +918,6 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () { return; } } - if ( - !this.storage.authProvider && // signup call, with - this.config.preventLoginWithUnverifiedEmail && // no login without verification - this.config.verifyUserEmails - ) { - // verification is on - this.storage.rejectSignup = true; - return; - } return this.createSessionToken(); }; From 2a6b0d7828854cfbbded30a55b155d6fcb243b72 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 19 Jun 2023 16:19:46 +1000 Subject: [PATCH 13/13] wip --- package-lock.json | 7 ++++++- src/RestWrite.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39a7a982ae..f1f77511cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "pluralize": "8.0.0", "rate-limit-redis": "3.0.2", "redis": "4.6.6", - "semver": "^7.5.1", + "semver": "7.5.1", "subscriptions-transport-ws": "0.11.0", "tv4": "1.3.0", "uuid": "9.0.0", @@ -15213,6 +15213,11 @@ "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, "engines": { "node": ">=0.10.0" } diff --git a/src/RestWrite.js b/src/RestWrite.js index 836e3ee6d3..003a4a7d0a 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -893,7 +893,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () { } if ( !this.storage.authProvider && // signup call, with - this.config.preventLoginWithUnverifiedEmail && // no login without verification + this.config.preventLoginWithUnverifiedEmail === true && // no login without verification this.config.verifyUserEmails ) { // verification is on