From 4adae7e4eff64e83ca4823258fa06641167efac9 Mon Sep 17 00:00:00 2001 From: Lenart Rudel Date: Mon, 21 Nov 2016 22:17:00 +0100 Subject: [PATCH 01/10] Add parseFrameURL for masking user-facing pages. Allow users to specify a different address which is used to mask parse requests for verifying email and resetting password. This is how Parse.com used to allow customers to gain control over page content, styling etc. On the destination page javascript is used to check the link in the request and embed the parse server page using IFRAME. --- src/Config.js | 4 ++++ src/Controllers/UserController.js | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Config.js b/src/Config.js index d2fa2726e9..7406baeef1 100644 --- a/src/Config.js +++ b/src/Config.js @@ -236,6 +236,10 @@ export class Config { return this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html`; } + get parseFrameURL() { + return this.customPages.parseFrameURL; + } + get verifyEmailURL() { return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`; } diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index d54f036b45..756c8667b2 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -120,7 +120,7 @@ export class UserController extends AdaptableController { // We may need to fetch the user in case of update email this.getUserIfNeeded(user).then((user) => { const username = encodeURIComponent(user.username); - let link = `${this.config.verifyEmailURL}?token=${token}&username=${username}`; + let link = buildVerificationLink(this.config.verifyEmailURL, username, token); let options = { appName: this.config.appName, link: link, @@ -155,8 +155,8 @@ export class UserController extends AdaptableController { .then(user => { const token = encodeURIComponent(user._perishable_token); const username = encodeURIComponent(user.username); - let link = `${this.config.requestResetPasswordURL}?token=${token}&username=${username}` + let link = buildVerificationLink(this.config.requestResetPasswordURL, username, token); let options = { appName: this.config.appName, link: link, @@ -217,4 +217,15 @@ function updateUserPassword(userId, password, config) { }); } +function buildVerificationLink(destination, username, token) { + let usernameAndToken = `token=${token}&username=${username}` + + if (this.config.parseFrameURL) { + let destinationWithoutHost = destination.replace(this.config.publicServerURL, ''); + return `${this.config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`; + } else { + return `${destination}?${usernameAndToken}`; + } +} + export default UserController; From 56d76f1ebc02e1f052ba0980d7a26e202c748e48 Mon Sep 17 00:00:00 2001 From: Lenart Rudel Date: Tue, 22 Nov 2016 13:08:03 +0100 Subject: [PATCH 02/10] Fix code indentation --- src/Controllers/UserController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 756c8667b2..9f631a11c8 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -222,7 +222,7 @@ function buildVerificationLink(destination, username, token) { if (this.config.parseFrameURL) { let destinationWithoutHost = destination.replace(this.config.publicServerURL, ''); - return `${this.config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`; + return `${this.config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`; } else { return `${destination}?${usernameAndToken}`; } From d8d233bf2a39ae89cf872e39ce8a80de16d1f496 Mon Sep 17 00:00:00 2001 From: Lenart Rudel Date: Wed, 23 Nov 2016 09:42:56 +0100 Subject: [PATCH 03/10] Rename method for building link and pass config to it. --- src/Controllers/UserController.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 9f631a11c8..242485be6d 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -120,7 +120,7 @@ export class UserController extends AdaptableController { // We may need to fetch the user in case of update email this.getUserIfNeeded(user).then((user) => { const username = encodeURIComponent(user.username); - let link = buildVerificationLink(this.config.verifyEmailURL, username, token); + let link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config); let options = { appName: this.config.appName, link: link, @@ -156,7 +156,7 @@ export class UserController extends AdaptableController { const token = encodeURIComponent(user._perishable_token); const username = encodeURIComponent(user.username); - let link = buildVerificationLink(this.config.requestResetPasswordURL, username, token); + let link = buildEmailLink(this.config.requestResetPasswordURL, username, token, this.config); let options = { appName: this.config.appName, link: link, @@ -217,12 +217,12 @@ function updateUserPassword(userId, password, config) { }); } -function buildVerificationLink(destination, username, token) { +function buildEmailLink(destination, username, token, config) { let usernameAndToken = `token=${token}&username=${username}` - if (this.config.parseFrameURL) { - let destinationWithoutHost = destination.replace(this.config.publicServerURL, ''); - return `${this.config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`; + if (config.parseFrameURL) { + let destinationWithoutHost = destination.replace(config.publicServerURL, ''); + return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`; } else { return `${destination}?${usernameAndToken}`; } From e419040b1148ab6c5ba73b1bda116be0dbf99937 Mon Sep 17 00:00:00 2001 From: Lenart Rudel Date: Wed, 23 Nov 2016 09:56:15 +0100 Subject: [PATCH 04/10] Add customPages options to README.md. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d95dbd7ef0..0ff8229846 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo * `revokeSessionOnPasswordReset` - When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions. * `accountLockout` - Lock account when a malicious user is attempting to determine an account password by trial and error. * `passwordPolicy` - Optional password policy rules to enforce. +* `customPages` - A hash with urls to override email verification links, password reset links and specify frame url for masking user-facing pages. Available keys: `parseFrameURL`, `invalidLink`, `choosePassword`, `passwordResetSuccess`, `verifyEmailSuccess`. ##### Logging From 704e81a4ec81c2b2f79bb3b7e0635af5afc4df19 Mon Sep 17 00:00:00 2001 From: Jure Triglav Date: Tue, 20 Dec 2016 15:43:04 +0100 Subject: [PATCH 05/10] Add tests for parseFrameURL email link building, and parseFrameURL option. --- spec/UserController.spec.js | 59 ++++++++++++++++++++++++ spec/ValidationAndPasswordsReset.spec.js | 4 +- src/Controllers/UserController.js | 4 +- 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 spec/UserController.spec.js diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js new file mode 100644 index 0000000000..2f8229f51f --- /dev/null +++ b/spec/UserController.spec.js @@ -0,0 +1,59 @@ +var UserController = require('../src/Controllers/UserController').UserController; +var emailAdapter = require('./MockEmailAdapter') +var AppCache = require('../src/cache').AppCache; + +describe('UserController', () => { + var user = { + _email_verify_token: 'testToken', + username: 'testUser', + email: 'test@example.com' + } + + describe('sendVerificationEmail', () => { + describe('parseFrameURL not provided', () => { + it('uses publicServerURL', (done) => { + + AppCache.put(defaultConfiguration.appId, Object.assign(defaultConfiguration, { + publicServerURL: 'http://www.example.com', + customPages: { + parseFrameURL: undefined + } + })) + + emailAdapter.sendVerificationEmail = (options) => { + expect(options.link).toEqual('http://www.example.com/apps/test/verify_email?token=testToken&username=testUser') + done() + } + + var userController = new UserController(emailAdapter, 'test', { + verifyUserEmails: true + }) + + userController.sendVerificationEmail(user) + }) + }) + + describe('parseFrameURL provided', () => { + it('uses parseFrameURL and includes the destination in the link parameter', (done) => { + + AppCache.put(defaultConfiguration.appId, Object.assign(defaultConfiguration, { + publicServerURL: 'http://www.example.com', + customPages: { + parseFrameURL: 'http://someother.example.com/handle-parse-iframe' + } + })) + + emailAdapter.sendVerificationEmail = (options) => { + expect(options.link).toEqual('http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser') + done() + } + + var userController = new UserController(emailAdapter, 'test', { + verifyUserEmails: true + }) + + userController.sendVerificationEmail(user) + }) + }) + }) +}); diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index f32813fb7b..cb4de0e686 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -12,7 +12,8 @@ describe("Custom Pages, Email Verification, Password Reset", () => { invalidLink: "myInvalidLink", verifyEmailSuccess: "myVerifyEmailSuccess", choosePassword: "myChoosePassword", - passwordResetSuccess: "myPasswordResetSuccess" + passwordResetSuccess: "myPasswordResetSuccess", + parseFrameURL: "http://example.com/handle-parse-iframe" }, publicServerURL: "https://my.public.server.com/1" }) @@ -22,6 +23,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => { expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess"); expect(config.choosePasswordURL).toEqual("myChoosePassword"); expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess"); + expect(config.parseFrameURL).toEqual("http://example.com/handle-parse-iframe"); expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email"); expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset"); done(); diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 705e58df0a..bcb992501f 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -217,10 +217,10 @@ function updateUserPassword(userId, password, config) { } function buildEmailLink(destination, username, token, config) { - let usernameAndToken = `token=${token}&username=${username}` + const usernameAndToken = `token=${token}&username=${username}` if (config.parseFrameURL) { - let destinationWithoutHost = destination.replace(config.publicServerURL, ''); + const destinationWithoutHost = destination.replace(config.publicServerURL, ''); return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`; } else { return `${destination}?${usernameAndToken}`; From 5e3368fdafa007a1de9415cfc9ea4c5a625ba6f3 Mon Sep 17 00:00:00 2001 From: Lenart Rudel Date: Mon, 21 Nov 2016 22:17:00 +0100 Subject: [PATCH 06/10] Add parseFrameURL for masking user-facing pages. Allow users to specify a different address which is used to mask parse requests for verifying email and resetting password. This is how Parse.com used to allow customers to gain control over page content, styling etc. On the destination page javascript is used to check the link in the request and embed the parse server page using IFRAME. --- src/Config.js | 4 ++++ src/Controllers/UserController.js | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Config.js b/src/Config.js index ed0b51248b..453c9f38d4 100644 --- a/src/Config.js +++ b/src/Config.js @@ -240,6 +240,10 @@ export class Config { return this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html`; } + get parseFrameURL() { + return this.customPages.parseFrameURL; + } + get verifyEmailURL() { return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`; } diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 48b4c7f00b..ad0d6753ad 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -119,7 +119,7 @@ export class UserController extends AdaptableController { // We may need to fetch the user in case of update email this.getUserIfNeeded(user).then((user) => { const username = encodeURIComponent(user.username); - const link = `${this.config.verifyEmailURL}?token=${token}&username=${username}`; + const link = buildVerificationLink(this.config.verifyEmailURL, username, token); const options = { appName: this.config.appName, link: link, @@ -153,8 +153,8 @@ export class UserController extends AdaptableController { .then(user => { const token = encodeURIComponent(user._perishable_token); const username = encodeURIComponent(user.username); - const link = `${this.config.requestResetPasswordURL}?token=${token}&username=${username}` + const link = buildVerificationLink(this.config.requestResetPasswordURL, username, token); const options = { appName: this.config.appName, link: link, @@ -215,4 +215,15 @@ function updateUserPassword(userId, password, config) { }); } +function buildVerificationLink(destination, username, token) { + let usernameAndToken = `token=${token}&username=${username}` + + if (this.config.parseFrameURL) { + let destinationWithoutHost = destination.replace(this.config.publicServerURL, ''); + return `${this.config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`; + } else { + return `${destination}?${usernameAndToken}`; + } +} + export default UserController; From af2595b3c366ff64ce99b132051daeafc4ac73e1 Mon Sep 17 00:00:00 2001 From: Lenart Rudel Date: Tue, 22 Nov 2016 13:08:03 +0100 Subject: [PATCH 07/10] Fix code indentation --- src/Controllers/UserController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index ad0d6753ad..1e38a4f197 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -220,7 +220,7 @@ function buildVerificationLink(destination, username, token) { if (this.config.parseFrameURL) { let destinationWithoutHost = destination.replace(this.config.publicServerURL, ''); - return `${this.config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`; + return `${this.config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`; } else { return `${destination}?${usernameAndToken}`; } From ed31b23d66978e54998d0ff38ddda88530bc5738 Mon Sep 17 00:00:00 2001 From: Lenart Rudel Date: Wed, 23 Nov 2016 09:42:56 +0100 Subject: [PATCH 08/10] Rename method for building link and pass config to it. --- src/Controllers/UserController.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 1e38a4f197..f918789d06 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -119,7 +119,7 @@ export class UserController extends AdaptableController { // We may need to fetch the user in case of update email this.getUserIfNeeded(user).then((user) => { const username = encodeURIComponent(user.username); - const link = buildVerificationLink(this.config.verifyEmailURL, username, token); + const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config); const options = { appName: this.config.appName, link: link, @@ -154,7 +154,7 @@ export class UserController extends AdaptableController { const token = encodeURIComponent(user._perishable_token); const username = encodeURIComponent(user.username); - const link = buildVerificationLink(this.config.requestResetPasswordURL, username, token); + const link = buildEmailLink(this.config.requestResetPasswordURL, username, token, this.config); const options = { appName: this.config.appName, link: link, @@ -215,12 +215,12 @@ function updateUserPassword(userId, password, config) { }); } -function buildVerificationLink(destination, username, token) { +function buildEmailLink(destination, username, token, config) { let usernameAndToken = `token=${token}&username=${username}` - if (this.config.parseFrameURL) { - let destinationWithoutHost = destination.replace(this.config.publicServerURL, ''); - return `${this.config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`; + if (config.parseFrameURL) { + let destinationWithoutHost = destination.replace(config.publicServerURL, ''); + return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`; } else { return `${destination}?${usernameAndToken}`; } From f6fd93fdf86166d5bf2bbac87c1b599e802b99ed Mon Sep 17 00:00:00 2001 From: Lenart Rudel Date: Wed, 23 Nov 2016 09:56:15 +0100 Subject: [PATCH 09/10] Add customPages options to README.md. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b7eab1dacc..e5a2561370 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo * `revokeSessionOnPasswordReset` - When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions. * `accountLockout` - Lock account when a malicious user is attempting to determine an account password by trial and error. * `passwordPolicy` - Optional password policy rules to enforce. +* `customPages` - A hash with urls to override email verification links, password reset links and specify frame url for masking user-facing pages. Available keys: `parseFrameURL`, `invalidLink`, `choosePassword`, `passwordResetSuccess`, `verifyEmailSuccess`. ##### Logging From 3e005a4bb384b64d664be62ad378753e79f99d0b Mon Sep 17 00:00:00 2001 From: Jure Triglav Date: Wed, 21 Dec 2016 10:30:03 +0100 Subject: [PATCH 10/10] Don't Object.assign to defaultConfiguration global --- spec/UserController.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 2f8229f51f..498f70b655 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -13,7 +13,7 @@ describe('UserController', () => { describe('parseFrameURL not provided', () => { it('uses publicServerURL', (done) => { - AppCache.put(defaultConfiguration.appId, Object.assign(defaultConfiguration, { + AppCache.put(defaultConfiguration.appId, Object.assign({}, defaultConfiguration, { publicServerURL: 'http://www.example.com', customPages: { parseFrameURL: undefined @@ -36,7 +36,7 @@ describe('UserController', () => { describe('parseFrameURL provided', () => { it('uses parseFrameURL and includes the destination in the link parameter', (done) => { - AppCache.put(defaultConfiguration.appId, Object.assign(defaultConfiguration, { + AppCache.put(defaultConfiguration.appId, Object.assign({}, defaultConfiguration, { publicServerURL: 'http://www.example.com', customPages: { parseFrameURL: 'http://someother.example.com/handle-parse-iframe'