From 8d4528e6c8feb4b3d4ac22a8fa4c874ac843add5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 22 Jun 2025 21:51:39 +1000 Subject: [PATCH 01/21] feature: add dynamic publicServerUrl --- spec/index.spec.js | 11 +++++++++++ src/Config.js | 32 ++++++++++++++++++++++++++++++-- src/middlewares.js | 1 + types/Options/index.d.ts | 2 +- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index 5093a6ea25..be2b86b7de 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -615,6 +615,17 @@ describe('server', () => { expect(config.masterKeyCache.expiresAt.getTime()).toBeGreaterThan(Date.now()); }); + it('should load publicServerURL', async () => { + await reconfigureServer({ + publicServerURL: () => 'https://myserver.com/1', + }); + + await new Parse.Object('TestObject').save(); + + const config = Config.get(Parse.applicationId); + expect(config.publicServerURL).toEqual('https://myserver.com/1'); + }); + it('should not reload if ttl is not set', async () => { const masterKeySpy = jasmine.createSpy().and.returnValue(Promise.resolve('initialMasterKey')); diff --git a/src/Config.js b/src/Config.js index bf6d50626c..1be243ae40 100644 --- a/src/Config.js +++ b/src/Config.js @@ -32,6 +32,7 @@ function removeTrailingSlash(str) { return str; } +const asyncKeys = ['publicServerURL']; export class Config { static get(applicationId: string, mount: string) { const cacheInfo = AppCache.get(applicationId); @@ -56,9 +57,33 @@ export class Config { return config; } + async loadKeys() { + const asyncKeys = ['publicServerURL']; + + await Promise.all( + asyncKeys.map(async key => { + if (typeof this[`_${key}`] === 'function') { + this[key] = await this[`_${key}`](); + } + }) + ); + + Config.put(this); + } + + static transformConfiguration(serverConfiguration) { + for (const key of Object.keys(serverConfiguration)) { + if (asyncKeys.includes(key) && typeof serverConfiguration[key] === 'function') { + serverConfiguration[`_${key}`] = serverConfiguration[key]; + delete serverConfiguration[key]; + } + } + } + static put(serverConfiguration) { Config.validateOptions(serverConfiguration); Config.validateControllers(serverConfiguration); + Config.transformConfiguration(serverConfiguration); AppCache.put(serverConfiguration.appId, serverConfiguration); Config.setupPasswordValidator(serverConfiguration.passwordPolicy); return serverConfiguration; @@ -116,7 +141,11 @@ export class Config { } if (publicServerURL) { - if (!publicServerURL.startsWith('http://') && !publicServerURL.startsWith('https://')) { + if ( + typeof publicServerURL !== 'function' && + !publicServerURL.startsWith('http://') && + !publicServerURL.startsWith('https://') + ) { throw 'publicServerURL should be a valid HTTPS URL starting with https://'; } } @@ -757,7 +786,6 @@ export class Config { return this.masterKey; } - // TODO: Remove this function once PagesRouter replaces the PublicAPIRouter; // the (default) endpoint has to be defined in PagesRouter only. get pagesEndpoint() { diff --git a/src/middlewares.js b/src/middlewares.js index bf8029844a..d660063297 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -213,6 +213,7 @@ export async function handleParseHeaders(req, res, next) { }); return; } + await config.loadKeys(); info.app = AppCache.get(info.appId); req.config = config; diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index ac1c71e886..165a411545 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -85,7 +85,7 @@ export interface ParseServerOptions { cacheAdapter?: Adapter; emailAdapter?: Adapter; encodeParseObjectInCloudFunction?: boolean; - publicServerURL?: string; + publicServerURL?: string | (() => string) | Promise; pages?: PagesOptions; customPages?: CustomPagesOptions; liveQuery?: LiveQueryOptions; From 3f4e3505d8c5d251a052a6fdd0eb2a78ec9eba3d Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 22 Jun 2025 22:13:26 +1000 Subject: [PATCH 02/21] Update Config.js --- src/Config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.js b/src/Config.js index 1be243ae40..98344f9ecb 100644 --- a/src/Config.js +++ b/src/Config.js @@ -68,7 +68,7 @@ export class Config { }) ); - Config.put(this); + AppCache.put(this.appId, this); } static transformConfiguration(serverConfiguration) { From c20b3d0680d67b7284e2215b6d17770d95aaf603 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 11 Aug 2025 11:23:26 +1000 Subject: [PATCH 03/21] Update index.spec.js --- spec/index.spec.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/spec/index.spec.js b/spec/index.spec.js index be2b86b7de..4f73cdd753 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -626,6 +626,43 @@ describe('server', () => { expect(config.publicServerURL).toEqual('https://myserver.com/1'); }); + it('should load publicServerURL from Promise', async () => { + await reconfigureServer({ + publicServerURL: () => Promise.resolve('https://async-server.com/1'), + }); + + await new Parse.Object('TestObject').save(); + + const config = Config.get(Parse.applicationId); + expect(config.publicServerURL).toEqual('https://async-server.com/1'); + }); + + it('should handle publicServerURL function throwing error', async () => { + const errorMessage = 'Failed to get public server URL'; + await reconfigureServer({ + publicServerURL: () => { + throw new Error(errorMessage); + }, + }); + + // The error should occur when trying to save an object (which triggers loadKeys in middleware) + await expectAsync( + new Parse.Object('TestObject').save() + ).toBeRejected(); + }); + + it('should handle publicServerURL Promise rejection', async () => { + const errorMessage = 'Async fetch of public server URL failed'; + await reconfigureServer({ + publicServerURL: () => Promise.reject(new Error(errorMessage)), + }); + + // The error should occur when trying to save an object (which triggers loadKeys in middleware) + await expectAsync( + new Parse.Object('TestObject').save() + ).toBeRejected(); + }); + it('should not reload if ttl is not set', async () => { const masterKeySpy = jasmine.createSpy().and.returnValue(Promise.resolve('initialMasterKey')); From 7890722f1573751b7e1a56cf456cec3dda4c9b0e Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:07:54 +0100 Subject: [PATCH 04/21] Apply suggestions from code review Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- spec/index.spec.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/index.spec.js b/spec/index.spec.js index 4f73cdd753..07e016dbdf 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -662,6 +662,34 @@ describe('server', () => { new Parse.Object('TestObject').save() ).toBeRejected(); }); + + it('should execute publicServerURL function on every access', async () => { + let counter = 0; + await reconfigureServer({ + publicServerURL: () => { + counter++; + return `https://server-${counter}.com/1`; + }, + }); + + // First request - should call the function + await new Parse.Object('TestObject').save(); + const config1 = Config.get(Parse.applicationId); + expect(config1.publicServerURL).toEqual('https://server-1.com/1'); + expect(counter).toEqual(1); + + // Second request - should call the function again + await new Parse.Object('TestObject').save(); + const config2 = Config.get(Parse.applicationId); + expect(config2.publicServerURL).toEqual('https://server-2.com/1'); + expect(counter).toEqual(2); + + // Third request - should call the function again + await new Parse.Object('TestObject').save(); + const config3 = Config.get(Parse.applicationId); + expect(config3.publicServerURL).toEqual('https://server-3.com/1'); + expect(counter).toEqual(3); + }); it('should not reload if ttl is not set', async () => { const masterKeySpy = jasmine.createSpy().and.returnValue(Promise.resolve('initialMasterKey')); From 9aa8735ed22818db3a51fc92dcfbb0566e100d2d Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:09:23 +0100 Subject: [PATCH 05/21] Apply suggestions from code review Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- spec/index.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index 07e016dbdf..c9647579b3 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -668,26 +668,26 @@ describe('server', () => { await reconfigureServer({ publicServerURL: () => { counter++; - return `https://server-${counter}.com/1`; + return `https://example.com/${counter}`; }, }); // First request - should call the function await new Parse.Object('TestObject').save(); const config1 = Config.get(Parse.applicationId); - expect(config1.publicServerURL).toEqual('https://server-1.com/1'); + expect(config1.publicServerURL).toEqual('https://example.com/1'); expect(counter).toEqual(1); // Second request - should call the function again await new Parse.Object('TestObject').save(); const config2 = Config.get(Parse.applicationId); - expect(config2.publicServerURL).toEqual('https://server-2.com/1'); + expect(config2.publicServerURL).toEqual('https://example.com/2'); expect(counter).toEqual(2); // Third request - should call the function again await new Parse.Object('TestObject').save(); const config3 = Config.get(Parse.applicationId); - expect(config3.publicServerURL).toEqual('https://server-3.com/1'); + expect(config3.publicServerURL).toEqual('https://example.com/3'); expect(counter).toEqual(3); }); From 7ff5df34c4963700d7d509eafc96ac4ea813d693 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:10:21 +0100 Subject: [PATCH 06/21] Apply suggestions from code review Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- spec/index.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index c9647579b3..e3bfe6b054 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -617,24 +617,24 @@ describe('server', () => { it('should load publicServerURL', async () => { await reconfigureServer({ - publicServerURL: () => 'https://myserver.com/1', + publicServerURL: () => 'https://example.com/1', }); await new Parse.Object('TestObject').save(); const config = Config.get(Parse.applicationId); - expect(config.publicServerURL).toEqual('https://myserver.com/1'); + expect(config.publicServerURL).toEqual('https://example.com/1'); }); it('should load publicServerURL from Promise', async () => { await reconfigureServer({ - publicServerURL: () => Promise.resolve('https://async-server.com/1'), + publicServerURL: () => Promise.resolve('https://example.com/1'), }); await new Parse.Object('TestObject').save(); const config = Config.get(Parse.applicationId); - expect(config.publicServerURL).toEqual('https://async-server.com/1'); + expect(config.publicServerURL).toEqual('https://example.com/1'); }); it('should handle publicServerURL function throwing error', async () => { From b85ed20fbffc479bee8fc2d403f7b244b7b78bbc Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:41:11 +0100 Subject: [PATCH 07/21] refactor tests --- spec/index.spec.js | 154 +++++++++++++++++++++++---------------------- 1 file changed, 78 insertions(+), 76 deletions(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index e3bfe6b054..cdb84ce249 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -615,82 +615,6 @@ describe('server', () => { expect(config.masterKeyCache.expiresAt.getTime()).toBeGreaterThan(Date.now()); }); - it('should load publicServerURL', async () => { - await reconfigureServer({ - publicServerURL: () => 'https://example.com/1', - }); - - await new Parse.Object('TestObject').save(); - - const config = Config.get(Parse.applicationId); - expect(config.publicServerURL).toEqual('https://example.com/1'); - }); - - it('should load publicServerURL from Promise', async () => { - await reconfigureServer({ - publicServerURL: () => Promise.resolve('https://example.com/1'), - }); - - await new Parse.Object('TestObject').save(); - - const config = Config.get(Parse.applicationId); - expect(config.publicServerURL).toEqual('https://example.com/1'); - }); - - it('should handle publicServerURL function throwing error', async () => { - const errorMessage = 'Failed to get public server URL'; - await reconfigureServer({ - publicServerURL: () => { - throw new Error(errorMessage); - }, - }); - - // The error should occur when trying to save an object (which triggers loadKeys in middleware) - await expectAsync( - new Parse.Object('TestObject').save() - ).toBeRejected(); - }); - - it('should handle publicServerURL Promise rejection', async () => { - const errorMessage = 'Async fetch of public server URL failed'; - await reconfigureServer({ - publicServerURL: () => Promise.reject(new Error(errorMessage)), - }); - - // The error should occur when trying to save an object (which triggers loadKeys in middleware) - await expectAsync( - new Parse.Object('TestObject').save() - ).toBeRejected(); - }); - - it('should execute publicServerURL function on every access', async () => { - let counter = 0; - await reconfigureServer({ - publicServerURL: () => { - counter++; - return `https://example.com/${counter}`; - }, - }); - - // First request - should call the function - await new Parse.Object('TestObject').save(); - const config1 = Config.get(Parse.applicationId); - expect(config1.publicServerURL).toEqual('https://example.com/1'); - expect(counter).toEqual(1); - - // Second request - should call the function again - await new Parse.Object('TestObject').save(); - const config2 = Config.get(Parse.applicationId); - expect(config2.publicServerURL).toEqual('https://example.com/2'); - expect(counter).toEqual(2); - - // Third request - should call the function again - await new Parse.Object('TestObject').save(); - const config3 = Config.get(Parse.applicationId); - expect(config3.publicServerURL).toEqual('https://example.com/3'); - expect(counter).toEqual(3); - }); - it('should not reload if ttl is not set', async () => { const masterKeySpy = jasmine.createSpy().and.returnValue(Promise.resolve('initialMasterKey')); @@ -761,4 +685,82 @@ describe('server', () => { }) .catch(done.fail); }); + + fdescribe('publicServerURL', () => { + it('should load publicServerURL', async () => { + await reconfigureServer({ + publicServerURL: () => 'https://example.com/1', + }); + + await new Parse.Object('TestObject').save(); + + const config = Config.get(Parse.applicationId); + expect(config.publicServerURL).toEqual('https://example.com/1'); + }); + + it('should load publicServerURL from Promise', async () => { + await reconfigureServer({ + publicServerURL: () => Promise.resolve('https://example.com/1'), + }); + + await new Parse.Object('TestObject').save(); + + const config = Config.get(Parse.applicationId); + expect(config.publicServerURL).toEqual('https://example.com/1'); + }); + + it('should handle publicServerURL function throwing error', async () => { + const errorMessage = 'Failed to get public server URL'; + await reconfigureServer({ + publicServerURL: () => { + throw new Error(errorMessage); + }, + }); + + // The error should occur when trying to save an object (which triggers loadKeys in middleware) + await expectAsync( + new Parse.Object('TestObject').save() + ).toBeRejected(); + }); + + it('should handle publicServerURL Promise rejection', async () => { + const errorMessage = 'Async fetch of public server URL failed'; + await reconfigureServer({ + publicServerURL: () => Promise.reject(new Error(errorMessage)), + }); + + // The error should occur when trying to save an object (which triggers loadKeys in middleware) + await expectAsync( + new Parse.Object('TestObject').save() + ).toBeRejected(); + }); + + it('should execute publicServerURL function on every access', async () => { + let counter = 0; + await reconfigureServer({ + publicServerURL: () => { + counter++; + return `https://example.com/${counter}`; + }, + }); + + // First request - should call the function + await new Parse.Object('TestObject').save(); + const config1 = Config.get(Parse.applicationId); + expect(config1.publicServerURL).toEqual('https://example.com/1'); + expect(counter).toEqual(1); + + // Second request - should call the function again + await new Parse.Object('TestObject').save(); + const config2 = Config.get(Parse.applicationId); + expect(config2.publicServerURL).toEqual('https://example.com/2'); + expect(counter).toEqual(2); + + // Third request - should call the function again + await new Parse.Object('TestObject').save(); + const config3 = Config.get(Parse.applicationId); + expect(config3.publicServerURL).toEqual('https://example.com/3'); + expect(counter).toEqual(3); + }); + }); }); From 44843e371c54015610c32258f7ab4d044ab9ae62 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:57:55 +0100 Subject: [PATCH 08/21] add tests --- spec/index.spec.js | 91 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index cdb84ce249..7cb006657d 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -735,7 +735,7 @@ describe('server', () => { ).toBeRejected(); }); - it('should execute publicServerURL function on every access', async () => { + it('executes publicServerURL function on every config access', async () => { let counter = 0; await reconfigureServer({ publicServerURL: () => { @@ -762,5 +762,94 @@ describe('server', () => { expect(config3.publicServerURL).toEqual('https://example.com/3'); expect(counter).toEqual(3); }); + + it('executes publicServerURL function on every password reset email', async () => { + let counter = 0; + const emailCalls = []; + + const emailAdapter = MockEmailAdapterWithOptions({ + sendPasswordResetEmail: ({ link }) => { + emailCalls.push(link); + return Promise.resolve(); + }, + }); + + await reconfigureServer({ + appName: 'test-app', + publicServerURL: () => { + counter++; + return `https://example.com/${counter}`; + }, + emailAdapter, + }); + + // Create a user + const user = new Parse.User(); + user.setUsername('user'); + user.setPassword('pass'); + user.setEmail('user@example.com'); + await user.signUp(); + + // Should use first publicServerURL + const counterBefore1 = counter; + await Parse.User.requestPasswordReset('user@example.com'); + await jasmine.timeout(); + expect(emailCalls.length).toEqual(1); + expect(emailCalls[0]).toContain(`https://example.com/${counterBefore1 + 1}`); + expect(counter).toBeGreaterThanOrEqual(2); + + // Should use updated publicServerURL + const counterBefore2 = counter; + await Parse.User.requestPasswordReset('user@example.com'); + await jasmine.timeout(); + expect(emailCalls.length).toEqual(2); + expect(emailCalls[1]).toContain(`https://example.com/${counterBefore2 + 1}`); + expect(counterBefore2).toBeGreaterThan(counterBefore1); + }); + + it('executes publicServerURL function on every verification email', async () => { + let counter = 0; + const emailCalls = []; + + const emailAdapter = MockEmailAdapterWithOptions({ + sendVerificationEmail: ({ link }) => { + emailCalls.push(link); + return Promise.resolve(); + }, + }); + + await reconfigureServer({ + appName: 'test-app', + verifyUserEmails: true, + publicServerURL: () => { + counter++; + return `https://example.com/${counter}`; + }, + emailAdapter, + }); + + // Should trigger verification email with first publicServerURL + const counterBefore1 = counter; + const user1 = new Parse.User(); + user1.setUsername('user1'); + user1.setPassword('pass1'); + user1.setEmail('user1@example.com'); + await user1.signUp(); + await jasmine.timeout(); + expect(emailCalls.length).toEqual(1); + expect(emailCalls[0]).toContain(`https://example.com/${counterBefore1 + 1}`); + + // Should trigger verification email with updated publicServerURL + const counterBefore2 = counter; + const user2 = new Parse.User(); + user2.setUsername('user2'); + user2.setPassword('pass2'); + user2.setEmail('user2@example.com'); + await user2.signUp(); + await jasmine.timeout(); + expect(emailCalls.length).toEqual(2); + expect(emailCalls[1]).toContain(`https://example.com/${counterBefore2 + 1}`); + expect(counterBefore2).toBeGreaterThan(counterBefore1); + }); }); }); From e85245af05a809acffc64ec0c6af0b601dcd2cd6 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:21:11 +0100 Subject: [PATCH 09/21] tmp --- src/Config.js | 79 +++++++++++++------------------ src/Controllers/UserController.js | 11 +++-- src/Routers/PagesRouter.js | 32 +++++++------ src/Routers/PublicAPIRouter.js | 31 ++++++------ src/batch.js | 5 +- src/middlewares.js | 20 +++++++- 6 files changed, 97 insertions(+), 81 deletions(-) diff --git a/src/Config.js b/src/Config.js index 98344f9ecb..afd09194a1 100644 --- a/src/Config.js +++ b/src/Config.js @@ -32,7 +32,6 @@ function removeTrailingSlash(str) { return str; } -const asyncKeys = ['publicServerURL']; export class Config { static get(applicationId: string, mount: string) { const cacheInfo = AppCache.get(applicationId); @@ -57,33 +56,16 @@ export class Config { return config; } - async loadKeys() { - const asyncKeys = ['publicServerURL']; - - await Promise.all( - asyncKeys.map(async key => { - if (typeof this[`_${key}`] === 'function') { - this[key] = await this[`_${key}`](); - } - }) - ); - - AppCache.put(this.appId, this); - } - - static transformConfiguration(serverConfiguration) { - for (const key of Object.keys(serverConfiguration)) { - if (asyncKeys.includes(key) && typeof serverConfiguration[key] === 'function') { - serverConfiguration[`_${key}`] = serverConfiguration[key]; - delete serverConfiguration[key]; - } + async getPublicServerURL() { + if (typeof this.publicServerURL === 'function') { + return await this.publicServerURL(); } + return this.publicServerURL; } static put(serverConfiguration) { Config.validateOptions(serverConfiguration); Config.validateControllers(serverConfiguration); - Config.transformConfiguration(serverConfiguration); AppCache.put(serverConfiguration.appId, serverConfiguration); Config.setupPasswordValidator(serverConfiguration.passwordPolicy); return serverConfiguration; @@ -474,7 +456,7 @@ export class Config { if (typeof appName !== 'string') { throw 'An app name is required for e-mail verification and password resets.'; } - if (typeof publicServerURL !== 'string') { + if (!publicServerURL || (typeof publicServerURL !== 'string' && typeof publicServerURL !== 'function')) { throw 'A public server url is required for e-mail verification and password resets.'; } if (emailVerifyTokenValidityDuration) { @@ -546,11 +528,7 @@ export class Config { } get mount() { - var mount = this._mount; - if (this.publicServerURL) { - mount = this.publicServerURL; - } - return mount; + return this._mount; } set mount(newValue) { @@ -714,46 +692,54 @@ export class Config { } } - get invalidLinkURL() { - return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`; + async invalidLinkURL() { + const publicServerURL = await this.getPublicServerURL(); + return this.customPages.invalidLink || `${publicServerURL}/apps/invalid_link.html`; } - get invalidVerificationLinkURL() { + async invalidVerificationLinkURL() { + const publicServerURL = await this.getPublicServerURL(); return ( this.customPages.invalidVerificationLink || - `${this.publicServerURL}/apps/invalid_verification_link.html` + `${publicServerURL}/apps/invalid_verification_link.html` ); } - get linkSendSuccessURL() { + async linkSendSuccessURL() { + const publicServerURL = await this.getPublicServerURL(); return ( - this.customPages.linkSendSuccess || `${this.publicServerURL}/apps/link_send_success.html` + this.customPages.linkSendSuccess || `${publicServerURL}/apps/link_send_success.html` ); } - get linkSendFailURL() { - return this.customPages.linkSendFail || `${this.publicServerURL}/apps/link_send_fail.html`; + async linkSendFailURL() { + const publicServerURL = await this.getPublicServerURL(); + return this.customPages.linkSendFail || `${publicServerURL}/apps/link_send_fail.html`; } - get verifyEmailSuccessURL() { + async verifyEmailSuccessURL() { + const publicServerURL = await this.getPublicServerURL(); return ( this.customPages.verifyEmailSuccess || - `${this.publicServerURL}/apps/verify_email_success.html` + `${publicServerURL}/apps/verify_email_success.html` ); } - get choosePasswordURL() { - return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`; + async choosePasswordURL() { + const publicServerURL = await this.getPublicServerURL(); + return this.customPages.choosePassword || `${publicServerURL}/apps/choose_password`; } - get requestResetPasswordURL() { - return `${this.publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/request_password_reset`; + async requestResetPasswordURL() { + const publicServerURL = await this.getPublicServerURL(); + return `${publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/request_password_reset`; } - get passwordResetSuccessURL() { + async passwordResetSuccessURL() { + const publicServerURL = await this.getPublicServerURL(); return ( this.customPages.passwordResetSuccess || - `${this.publicServerURL}/apps/password_reset_success.html` + `${publicServerURL}/apps/password_reset_success.html` ); } @@ -761,8 +747,9 @@ export class Config { return this.customPages.parseFrameURL; } - get verifyEmailURL() { - return `${this.publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/verify_email`; + async verifyEmailURL() { + const publicServerURL = await this.getPublicServerURL(); + return `${publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/verify_email`; } async loadMasterKey() { diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 296b7f6868..931e482dc6 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -171,7 +171,8 @@ export class UserController extends AdaptableController { if (!shouldSendEmail) { return; } - const link = buildEmailLink(this.config.verifyEmailURL, token, this.config); + const verifyEmailURL = await this.config.verifyEmailURL(); + const link = await buildEmailLink(verifyEmailURL, token, this.config); const options = { appName: this.config.appName, link: link, @@ -282,7 +283,8 @@ export class UserController extends AdaptableController { user = await this.setPasswordResetToken(email); } const token = encodeURIComponent(user._perishable_token); - const link = buildEmailLink(this.config.requestResetPasswordURL, token, this.config); + const requestResetPasswordURL = await this.config.requestResetPasswordURL(); + const link = await buildEmailLink(requestResetPasswordURL, token, this.config); const options = { appName: this.config.appName, link: link, @@ -361,10 +363,11 @@ function updateUserPassword(user, password, config) { .then(() => user); } -function buildEmailLink(destination, token, config) { +async function buildEmailLink(destination, token, config) { token = `token=${token}`; if (config.parseFrameURL) { - const destinationWithoutHost = destination.replace(config.publicServerURL, ''); + const publicServerURL = await config.getPublicServerURL(); + const destinationWithoutHost = destination.replace(publicServerURL, ''); return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${token}`; } else { diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 1ea3211684..fd48d6a6f0 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -130,14 +130,15 @@ export class PagesRouter extends PromiseRouter { ); } - passwordReset(req) { + async passwordReset(req) { const config = req.config; + const publicServerURL = await config.getPublicServerURL(); const params = { [pageParams.appId]: req.params.appId, [pageParams.appName]: config.appName, [pageParams.token]: req.query.token, [pageParams.username]: req.query.username, - [pageParams.publicServerUrl]: config.publicServerURL, + [pageParams.publicServerUrl]: publicServerURL, }; return this.goToPage(req, pages.passwordReset, params); } @@ -255,7 +256,7 @@ export class PagesRouter extends PromiseRouter { * - POST request -> redirect response (PRG pattern) * @returns {Promise} The PromiseRouter response. */ - goToPage(req, page, params = {}, responseType) { + async goToPage(req, page, params = {}, responseType) { const config = req.config; // Determine redirect either by force, response setting or request method @@ -266,7 +267,7 @@ export class PagesRouter extends PromiseRouter { : req.method == 'POST'; // Include default parameters - const defaultParams = this.getDefaultParams(config); + const defaultParams = await this.getDefaultParams(config); if (Object.values(defaultParams).includes(undefined)) { return this.notFound(); } @@ -281,7 +282,8 @@ export class PagesRouter extends PromiseRouter { // Compose paths and URLs const defaultFile = page.defaultFile; const defaultPath = this.defaultPagePath(defaultFile); - const defaultUrl = this.composePageUrl(defaultFile, config.publicServerURL); + const publicServerURL = await config.getPublicServerURL(); + const defaultUrl = this.composePageUrl(defaultFile, publicServerURL); // If custom URL is set redirect to it without localization const customUrl = config.pages.customUrls[page.id]; @@ -300,7 +302,7 @@ export class PagesRouter extends PromiseRouter { return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => redirect ? this.redirectResponse( - this.composePageUrl(defaultFile, config.publicServerURL, subdir), + this.composePageUrl(defaultFile, publicServerURL, subdir), params ) : this.pageResponse(path, params, placeholders) @@ -529,14 +531,16 @@ export class PagesRouter extends PromiseRouter { * @param {Object} config The Parse Server configuration. * @returns {Object} The default parameters. */ - getDefaultParams(config) { - return config - ? { - [pageParams.appId]: config.appId, - [pageParams.appName]: config.appName, - [pageParams.publicServerUrl]: config.publicServerURL, - } - : {}; + async getDefaultParams(config) { + if (!config) { + return {}; + } + const publicServerURL = await config.getPublicServerURL(); + return { + [pageParams.appId]: config.appId, + [pageParams.appName]: config.appName, + [pageParams.publicServerUrl]: publicServerURL, + }; } /** diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 2ec993f390..4268d39191 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -88,26 +88,29 @@ export class PublicAPIRouter extends PromiseRouter { ); } - changePassword(req) { - return new Promise((resolve, reject) => { - const config = Config.get(req.query.id); + async changePassword(req) { + const config = Config.get(req.query.id); - if (!config) { - this.invalidRequest(); - } + if (!config) { + this.invalidRequest(); + } - if (!config.publicServerURL) { - return resolve({ - status: 404, - text: 'Not found.', - }); - } - // Should we keep the file in memory or leave like that? + if (!config.publicServerURL) { + return { + status: 404, + text: 'Not found.', + }; + } + + const publicServerURL = await config.getPublicServerURL(); + + // Should we keep the file in memory or leave like that? + return new Promise((resolve, reject) => { fs.readFile(path.resolve(views, 'choose_password'), 'utf-8', (err, data) => { if (err) { return reject(err); } - data = data.replace('PARSE_SERVER_URL', `'${config.publicServerURL}'`); + data = data.replace('PARSE_SERVER_URL', `'${publicServerURL}'`); resolve({ text: data, }); diff --git a/src/batch.js b/src/batch.js index 80fa028cc6..cbd3ff9b90 100644 --- a/src/batch.js +++ b/src/batch.js @@ -63,7 +63,7 @@ function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) { // Returns a promise for a {response} object. // TODO: pass along auth correctly -function handleBatch(router, req) { +async function handleBatch(router, req) { if (!Array.isArray(req.body?.requests)) { throw new Parse.Error(Parse.Error.INVALID_JSON, 'requests must be an array'); } @@ -77,10 +77,11 @@ function handleBatch(router, req) { throw 'internal routing problem - expected url to end with batch'; } + const publicServerURL = await req.config.getPublicServerURL(); const makeRoutablePath = makeBatchRoutingPathFunction( req.originalUrl, req.config.serverURL, - req.config.publicServerURL + publicServerURL ); const batch = transactionRetries => { diff --git a/src/middlewares.js b/src/middlewares.js index 93b16f3846..389e3d0ff8 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -213,7 +213,25 @@ export async function handleParseHeaders(req, res, next) { }); return; } - await config.loadKeys(); + + // Execute publicServerURL function if it's a function + if (typeof config.publicServerURL === 'function') { + // Store the function for next request and resolve it for this request + const urlFunction = config.publicServerURL; + const resolvedURL = await urlFunction(); + config.publicServerURL = resolvedURL; + // Update the cached config with resolved value + const cachedConfig = AppCache.get(info.appId); + cachedConfig.publicServerURL = resolvedURL; + // But keep the function for next time + cachedConfig._publicServerURLFunction = urlFunction; + } else if (config._publicServerURLFunction) { + // Function was previously stored, execute it again + const resolvedURL = await config._publicServerURLFunction(); + config.publicServerURL = resolvedURL; + const cachedConfig = AppCache.get(info.appId); + cachedConfig.publicServerURL = resolvedURL; + } info.app = AppCache.get(info.appId); req.config = config; From c17632c6feb22394b3376c12fc6b882fcf158015 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:56:35 +0100 Subject: [PATCH 10/21] fix --- spec/index.spec.js | 2 +- src/Config.js | 88 ++++++++++++++++++++----------- src/Controllers/UserController.js | 11 ++-- src/middlewares.js | 20 +------ 4 files changed, 62 insertions(+), 59 deletions(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index 7cb006657d..91962252a7 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -686,7 +686,7 @@ describe('server', () => { .catch(done.fail); }); - fdescribe('publicServerURL', () => { + describe('publicServerURL', () => { it('should load publicServerURL', async () => { await reconfigureServer({ publicServerURL: () => 'https://example.com/1', diff --git a/src/Config.js b/src/Config.js index afd09194a1..b4a1537185 100644 --- a/src/Config.js +++ b/src/Config.js @@ -32,6 +32,25 @@ function removeTrailingSlash(str) { return str; } +// List of config keys that can be async (functions or promises) +const asyncKeys = ['publicServerURL']; + +/** + * Helper function to resolve an async config value. + * If the value is a function, it executes it and returns the result. + * If the value is a promise, it awaits it and returns the result. + * Otherwise, it returns the raw value. + */ +async function resolveAsyncValue(value) { + if (typeof value === 'function') { + return await value(); + } + if (value && typeof value.then === 'function') { + return await value; + } + return value; +} + export class Config { static get(applicationId: string, mount: string) { const cacheInfo = AppCache.get(applicationId); @@ -53,14 +72,28 @@ export class Config { config ); config.version = version; + + // Transform async keys: store original in _[key] + asyncKeys.forEach(key => { + if (config[key] !== undefined && (typeof config[key] === 'function' || (config[key] && typeof config[key].then === 'function'))) { + config[`_${key}`] = config[key]; + // Will be resolved in middleware + delete config[key]; + } + }); + return config; } - async getPublicServerURL() { - if (typeof this.publicServerURL === 'function') { - return await this.publicServerURL(); - } - return this.publicServerURL; + async loadKeys() { + await Promise.all( + asyncKeys.map(async key => { + if (this[`_${key}`] !== undefined) { + this[key] = await resolveAsyncValue(this[`_${key}`]); + } + }) + ); + AppCache.put(this.appId, this); } static put(serverConfiguration) { @@ -692,54 +725,46 @@ export class Config { } } - async invalidLinkURL() { - const publicServerURL = await this.getPublicServerURL(); - return this.customPages.invalidLink || `${publicServerURL}/apps/invalid_link.html`; + get invalidLinkURL() { + return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`; } - async invalidVerificationLinkURL() { - const publicServerURL = await this.getPublicServerURL(); + get invalidVerificationLinkURL() { return ( this.customPages.invalidVerificationLink || - `${publicServerURL}/apps/invalid_verification_link.html` + `${this.publicServerURL}/apps/invalid_verification_link.html` ); } - async linkSendSuccessURL() { - const publicServerURL = await this.getPublicServerURL(); + get linkSendSuccessURL() { return ( - this.customPages.linkSendSuccess || `${publicServerURL}/apps/link_send_success.html` + this.customPages.linkSendSuccess || `${this.publicServerURL}/apps/link_send_success.html` ); } - async linkSendFailURL() { - const publicServerURL = await this.getPublicServerURL(); - return this.customPages.linkSendFail || `${publicServerURL}/apps/link_send_fail.html`; + get linkSendFailURL() { + return this.customPages.linkSendFail || `${this.publicServerURL}/apps/link_send_fail.html`; } - async verifyEmailSuccessURL() { - const publicServerURL = await this.getPublicServerURL(); + get verifyEmailSuccessURL() { return ( this.customPages.verifyEmailSuccess || - `${publicServerURL}/apps/verify_email_success.html` + `${this.publicServerURL}/apps/verify_email_success.html` ); } - async choosePasswordURL() { - const publicServerURL = await this.getPublicServerURL(); - return this.customPages.choosePassword || `${publicServerURL}/apps/choose_password`; + get choosePasswordURL() { + return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`; } - async requestResetPasswordURL() { - const publicServerURL = await this.getPublicServerURL(); - return `${publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/request_password_reset`; + get requestResetPasswordURL() { + return `${this.publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/request_password_reset`; } - async passwordResetSuccessURL() { - const publicServerURL = await this.getPublicServerURL(); + get passwordResetSuccessURL() { return ( this.customPages.passwordResetSuccess || - `${publicServerURL}/apps/password_reset_success.html` + `${this.publicServerURL}/apps/password_reset_success.html` ); } @@ -747,9 +772,8 @@ export class Config { return this.customPages.parseFrameURL; } - async verifyEmailURL() { - const publicServerURL = await this.getPublicServerURL(); - return `${publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/verify_email`; + get verifyEmailURL() { + return `${this.publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/verify_email`; } async loadMasterKey() { diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 931e482dc6..296b7f6868 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -171,8 +171,7 @@ export class UserController extends AdaptableController { if (!shouldSendEmail) { return; } - const verifyEmailURL = await this.config.verifyEmailURL(); - const link = await buildEmailLink(verifyEmailURL, token, this.config); + const link = buildEmailLink(this.config.verifyEmailURL, token, this.config); const options = { appName: this.config.appName, link: link, @@ -283,8 +282,7 @@ export class UserController extends AdaptableController { user = await this.setPasswordResetToken(email); } const token = encodeURIComponent(user._perishable_token); - const requestResetPasswordURL = await this.config.requestResetPasswordURL(); - const link = await buildEmailLink(requestResetPasswordURL, token, this.config); + const link = buildEmailLink(this.config.requestResetPasswordURL, token, this.config); const options = { appName: this.config.appName, link: link, @@ -363,11 +361,10 @@ function updateUserPassword(user, password, config) { .then(() => user); } -async function buildEmailLink(destination, token, config) { +function buildEmailLink(destination, token, config) { token = `token=${token}`; if (config.parseFrameURL) { - const publicServerURL = await config.getPublicServerURL(); - const destinationWithoutHost = destination.replace(publicServerURL, ''); + const destinationWithoutHost = destination.replace(config.publicServerURL, ''); return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${token}`; } else { diff --git a/src/middlewares.js b/src/middlewares.js index 389e3d0ff8..93b16f3846 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -213,25 +213,7 @@ export async function handleParseHeaders(req, res, next) { }); return; } - - // Execute publicServerURL function if it's a function - if (typeof config.publicServerURL === 'function') { - // Store the function for next request and resolve it for this request - const urlFunction = config.publicServerURL; - const resolvedURL = await urlFunction(); - config.publicServerURL = resolvedURL; - // Update the cached config with resolved value - const cachedConfig = AppCache.get(info.appId); - cachedConfig.publicServerURL = resolvedURL; - // But keep the function for next time - cachedConfig._publicServerURLFunction = urlFunction; - } else if (config._publicServerURLFunction) { - // Function was previously stored, execute it again - const resolvedURL = await config._publicServerURLFunction(); - config.publicServerURL = resolvedURL; - const cachedConfig = AppCache.get(info.appId); - cachedConfig.publicServerURL = resolvedURL; - } + await config.loadKeys(); info.app = AppCache.get(info.appId); req.config = config; From f719294a4104d3fd3b3ae1e5ad723a0467200bb0 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:01:46 +0100 Subject: [PATCH 11/21] Revert "fix" This reverts commit c17632c6feb22394b3376c12fc6b882fcf158015. --- spec/index.spec.js | 2 +- src/Config.js | 88 +++++++++++-------------------- src/Controllers/UserController.js | 11 ++-- src/middlewares.js | 20 ++++++- 4 files changed, 59 insertions(+), 62 deletions(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index 91962252a7..7cb006657d 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -686,7 +686,7 @@ describe('server', () => { .catch(done.fail); }); - describe('publicServerURL', () => { + fdescribe('publicServerURL', () => { it('should load publicServerURL', async () => { await reconfigureServer({ publicServerURL: () => 'https://example.com/1', diff --git a/src/Config.js b/src/Config.js index b4a1537185..afd09194a1 100644 --- a/src/Config.js +++ b/src/Config.js @@ -32,25 +32,6 @@ function removeTrailingSlash(str) { return str; } -// List of config keys that can be async (functions or promises) -const asyncKeys = ['publicServerURL']; - -/** - * Helper function to resolve an async config value. - * If the value is a function, it executes it and returns the result. - * If the value is a promise, it awaits it and returns the result. - * Otherwise, it returns the raw value. - */ -async function resolveAsyncValue(value) { - if (typeof value === 'function') { - return await value(); - } - if (value && typeof value.then === 'function') { - return await value; - } - return value; -} - export class Config { static get(applicationId: string, mount: string) { const cacheInfo = AppCache.get(applicationId); @@ -72,28 +53,14 @@ export class Config { config ); config.version = version; - - // Transform async keys: store original in _[key] - asyncKeys.forEach(key => { - if (config[key] !== undefined && (typeof config[key] === 'function' || (config[key] && typeof config[key].then === 'function'))) { - config[`_${key}`] = config[key]; - // Will be resolved in middleware - delete config[key]; - } - }); - return config; } - async loadKeys() { - await Promise.all( - asyncKeys.map(async key => { - if (this[`_${key}`] !== undefined) { - this[key] = await resolveAsyncValue(this[`_${key}`]); - } - }) - ); - AppCache.put(this.appId, this); + async getPublicServerURL() { + if (typeof this.publicServerURL === 'function') { + return await this.publicServerURL(); + } + return this.publicServerURL; } static put(serverConfiguration) { @@ -725,46 +692,54 @@ export class Config { } } - get invalidLinkURL() { - return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`; + async invalidLinkURL() { + const publicServerURL = await this.getPublicServerURL(); + return this.customPages.invalidLink || `${publicServerURL}/apps/invalid_link.html`; } - get invalidVerificationLinkURL() { + async invalidVerificationLinkURL() { + const publicServerURL = await this.getPublicServerURL(); return ( this.customPages.invalidVerificationLink || - `${this.publicServerURL}/apps/invalid_verification_link.html` + `${publicServerURL}/apps/invalid_verification_link.html` ); } - get linkSendSuccessURL() { + async linkSendSuccessURL() { + const publicServerURL = await this.getPublicServerURL(); return ( - this.customPages.linkSendSuccess || `${this.publicServerURL}/apps/link_send_success.html` + this.customPages.linkSendSuccess || `${publicServerURL}/apps/link_send_success.html` ); } - get linkSendFailURL() { - return this.customPages.linkSendFail || `${this.publicServerURL}/apps/link_send_fail.html`; + async linkSendFailURL() { + const publicServerURL = await this.getPublicServerURL(); + return this.customPages.linkSendFail || `${publicServerURL}/apps/link_send_fail.html`; } - get verifyEmailSuccessURL() { + async verifyEmailSuccessURL() { + const publicServerURL = await this.getPublicServerURL(); return ( this.customPages.verifyEmailSuccess || - `${this.publicServerURL}/apps/verify_email_success.html` + `${publicServerURL}/apps/verify_email_success.html` ); } - get choosePasswordURL() { - return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`; + async choosePasswordURL() { + const publicServerURL = await this.getPublicServerURL(); + return this.customPages.choosePassword || `${publicServerURL}/apps/choose_password`; } - get requestResetPasswordURL() { - return `${this.publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/request_password_reset`; + async requestResetPasswordURL() { + const publicServerURL = await this.getPublicServerURL(); + return `${publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/request_password_reset`; } - get passwordResetSuccessURL() { + async passwordResetSuccessURL() { + const publicServerURL = await this.getPublicServerURL(); return ( this.customPages.passwordResetSuccess || - `${this.publicServerURL}/apps/password_reset_success.html` + `${publicServerURL}/apps/password_reset_success.html` ); } @@ -772,8 +747,9 @@ export class Config { return this.customPages.parseFrameURL; } - get verifyEmailURL() { - return `${this.publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/verify_email`; + async verifyEmailURL() { + const publicServerURL = await this.getPublicServerURL(); + return `${publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/verify_email`; } async loadMasterKey() { diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 296b7f6868..931e482dc6 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -171,7 +171,8 @@ export class UserController extends AdaptableController { if (!shouldSendEmail) { return; } - const link = buildEmailLink(this.config.verifyEmailURL, token, this.config); + const verifyEmailURL = await this.config.verifyEmailURL(); + const link = await buildEmailLink(verifyEmailURL, token, this.config); const options = { appName: this.config.appName, link: link, @@ -282,7 +283,8 @@ export class UserController extends AdaptableController { user = await this.setPasswordResetToken(email); } const token = encodeURIComponent(user._perishable_token); - const link = buildEmailLink(this.config.requestResetPasswordURL, token, this.config); + const requestResetPasswordURL = await this.config.requestResetPasswordURL(); + const link = await buildEmailLink(requestResetPasswordURL, token, this.config); const options = { appName: this.config.appName, link: link, @@ -361,10 +363,11 @@ function updateUserPassword(user, password, config) { .then(() => user); } -function buildEmailLink(destination, token, config) { +async function buildEmailLink(destination, token, config) { token = `token=${token}`; if (config.parseFrameURL) { - const destinationWithoutHost = destination.replace(config.publicServerURL, ''); + const publicServerURL = await config.getPublicServerURL(); + const destinationWithoutHost = destination.replace(publicServerURL, ''); return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${token}`; } else { diff --git a/src/middlewares.js b/src/middlewares.js index 93b16f3846..389e3d0ff8 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -213,7 +213,25 @@ export async function handleParseHeaders(req, res, next) { }); return; } - await config.loadKeys(); + + // Execute publicServerURL function if it's a function + if (typeof config.publicServerURL === 'function') { + // Store the function for next request and resolve it for this request + const urlFunction = config.publicServerURL; + const resolvedURL = await urlFunction(); + config.publicServerURL = resolvedURL; + // Update the cached config with resolved value + const cachedConfig = AppCache.get(info.appId); + cachedConfig.publicServerURL = resolvedURL; + // But keep the function for next time + cachedConfig._publicServerURLFunction = urlFunction; + } else if (config._publicServerURLFunction) { + // Function was previously stored, execute it again + const resolvedURL = await config._publicServerURLFunction(); + config.publicServerURL = resolvedURL; + const cachedConfig = AppCache.get(info.appId); + cachedConfig.publicServerURL = resolvedURL; + } info.app = AppCache.get(info.appId); req.config = config; From b76bbf40b8c1570dfaceb6727d36f3296554e5b1 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:04:11 +0100 Subject: [PATCH 12/21] Revert "tmp" This reverts commit e85245af05a809acffc64ec0c6af0b601dcd2cd6. --- src/Config.js | 79 ++++++++++++++++++------------- src/Controllers/UserController.js | 11 ++--- src/Routers/PagesRouter.js | 32 ++++++------- src/Routers/PublicAPIRouter.js | 31 ++++++------ src/batch.js | 5 +- src/middlewares.js | 20 +------- 6 files changed, 81 insertions(+), 97 deletions(-) diff --git a/src/Config.js b/src/Config.js index afd09194a1..98344f9ecb 100644 --- a/src/Config.js +++ b/src/Config.js @@ -32,6 +32,7 @@ function removeTrailingSlash(str) { return str; } +const asyncKeys = ['publicServerURL']; export class Config { static get(applicationId: string, mount: string) { const cacheInfo = AppCache.get(applicationId); @@ -56,16 +57,33 @@ export class Config { return config; } - async getPublicServerURL() { - if (typeof this.publicServerURL === 'function') { - return await this.publicServerURL(); + async loadKeys() { + const asyncKeys = ['publicServerURL']; + + await Promise.all( + asyncKeys.map(async key => { + if (typeof this[`_${key}`] === 'function') { + this[key] = await this[`_${key}`](); + } + }) + ); + + AppCache.put(this.appId, this); + } + + static transformConfiguration(serverConfiguration) { + for (const key of Object.keys(serverConfiguration)) { + if (asyncKeys.includes(key) && typeof serverConfiguration[key] === 'function') { + serverConfiguration[`_${key}`] = serverConfiguration[key]; + delete serverConfiguration[key]; + } } - return this.publicServerURL; } static put(serverConfiguration) { Config.validateOptions(serverConfiguration); Config.validateControllers(serverConfiguration); + Config.transformConfiguration(serverConfiguration); AppCache.put(serverConfiguration.appId, serverConfiguration); Config.setupPasswordValidator(serverConfiguration.passwordPolicy); return serverConfiguration; @@ -456,7 +474,7 @@ export class Config { if (typeof appName !== 'string') { throw 'An app name is required for e-mail verification and password resets.'; } - if (!publicServerURL || (typeof publicServerURL !== 'string' && typeof publicServerURL !== 'function')) { + if (typeof publicServerURL !== 'string') { throw 'A public server url is required for e-mail verification and password resets.'; } if (emailVerifyTokenValidityDuration) { @@ -528,7 +546,11 @@ export class Config { } get mount() { - return this._mount; + var mount = this._mount; + if (this.publicServerURL) { + mount = this.publicServerURL; + } + return mount; } set mount(newValue) { @@ -692,54 +714,46 @@ export class Config { } } - async invalidLinkURL() { - const publicServerURL = await this.getPublicServerURL(); - return this.customPages.invalidLink || `${publicServerURL}/apps/invalid_link.html`; + get invalidLinkURL() { + return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`; } - async invalidVerificationLinkURL() { - const publicServerURL = await this.getPublicServerURL(); + get invalidVerificationLinkURL() { return ( this.customPages.invalidVerificationLink || - `${publicServerURL}/apps/invalid_verification_link.html` + `${this.publicServerURL}/apps/invalid_verification_link.html` ); } - async linkSendSuccessURL() { - const publicServerURL = await this.getPublicServerURL(); + get linkSendSuccessURL() { return ( - this.customPages.linkSendSuccess || `${publicServerURL}/apps/link_send_success.html` + this.customPages.linkSendSuccess || `${this.publicServerURL}/apps/link_send_success.html` ); } - async linkSendFailURL() { - const publicServerURL = await this.getPublicServerURL(); - return this.customPages.linkSendFail || `${publicServerURL}/apps/link_send_fail.html`; + get linkSendFailURL() { + return this.customPages.linkSendFail || `${this.publicServerURL}/apps/link_send_fail.html`; } - async verifyEmailSuccessURL() { - const publicServerURL = await this.getPublicServerURL(); + get verifyEmailSuccessURL() { return ( this.customPages.verifyEmailSuccess || - `${publicServerURL}/apps/verify_email_success.html` + `${this.publicServerURL}/apps/verify_email_success.html` ); } - async choosePasswordURL() { - const publicServerURL = await this.getPublicServerURL(); - return this.customPages.choosePassword || `${publicServerURL}/apps/choose_password`; + get choosePasswordURL() { + return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`; } - async requestResetPasswordURL() { - const publicServerURL = await this.getPublicServerURL(); - return `${publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/request_password_reset`; + get requestResetPasswordURL() { + return `${this.publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/request_password_reset`; } - async passwordResetSuccessURL() { - const publicServerURL = await this.getPublicServerURL(); + get passwordResetSuccessURL() { return ( this.customPages.passwordResetSuccess || - `${publicServerURL}/apps/password_reset_success.html` + `${this.publicServerURL}/apps/password_reset_success.html` ); } @@ -747,9 +761,8 @@ export class Config { return this.customPages.parseFrameURL; } - async verifyEmailURL() { - const publicServerURL = await this.getPublicServerURL(); - return `${publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/verify_email`; + get verifyEmailURL() { + return `${this.publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/verify_email`; } async loadMasterKey() { diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 931e482dc6..296b7f6868 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -171,8 +171,7 @@ export class UserController extends AdaptableController { if (!shouldSendEmail) { return; } - const verifyEmailURL = await this.config.verifyEmailURL(); - const link = await buildEmailLink(verifyEmailURL, token, this.config); + const link = buildEmailLink(this.config.verifyEmailURL, token, this.config); const options = { appName: this.config.appName, link: link, @@ -283,8 +282,7 @@ export class UserController extends AdaptableController { user = await this.setPasswordResetToken(email); } const token = encodeURIComponent(user._perishable_token); - const requestResetPasswordURL = await this.config.requestResetPasswordURL(); - const link = await buildEmailLink(requestResetPasswordURL, token, this.config); + const link = buildEmailLink(this.config.requestResetPasswordURL, token, this.config); const options = { appName: this.config.appName, link: link, @@ -363,11 +361,10 @@ function updateUserPassword(user, password, config) { .then(() => user); } -async function buildEmailLink(destination, token, config) { +function buildEmailLink(destination, token, config) { token = `token=${token}`; if (config.parseFrameURL) { - const publicServerURL = await config.getPublicServerURL(); - const destinationWithoutHost = destination.replace(publicServerURL, ''); + const destinationWithoutHost = destination.replace(config.publicServerURL, ''); return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${token}`; } else { diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index fd48d6a6f0..1ea3211684 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -130,15 +130,14 @@ export class PagesRouter extends PromiseRouter { ); } - async passwordReset(req) { + passwordReset(req) { const config = req.config; - const publicServerURL = await config.getPublicServerURL(); const params = { [pageParams.appId]: req.params.appId, [pageParams.appName]: config.appName, [pageParams.token]: req.query.token, [pageParams.username]: req.query.username, - [pageParams.publicServerUrl]: publicServerURL, + [pageParams.publicServerUrl]: config.publicServerURL, }; return this.goToPage(req, pages.passwordReset, params); } @@ -256,7 +255,7 @@ export class PagesRouter extends PromiseRouter { * - POST request -> redirect response (PRG pattern) * @returns {Promise} The PromiseRouter response. */ - async goToPage(req, page, params = {}, responseType) { + goToPage(req, page, params = {}, responseType) { const config = req.config; // Determine redirect either by force, response setting or request method @@ -267,7 +266,7 @@ export class PagesRouter extends PromiseRouter { : req.method == 'POST'; // Include default parameters - const defaultParams = await this.getDefaultParams(config); + const defaultParams = this.getDefaultParams(config); if (Object.values(defaultParams).includes(undefined)) { return this.notFound(); } @@ -282,8 +281,7 @@ export class PagesRouter extends PromiseRouter { // Compose paths and URLs const defaultFile = page.defaultFile; const defaultPath = this.defaultPagePath(defaultFile); - const publicServerURL = await config.getPublicServerURL(); - const defaultUrl = this.composePageUrl(defaultFile, publicServerURL); + const defaultUrl = this.composePageUrl(defaultFile, config.publicServerURL); // If custom URL is set redirect to it without localization const customUrl = config.pages.customUrls[page.id]; @@ -302,7 +300,7 @@ export class PagesRouter extends PromiseRouter { return Utils.getLocalizedPath(defaultPath, locale).then(({ path, subdir }) => redirect ? this.redirectResponse( - this.composePageUrl(defaultFile, publicServerURL, subdir), + this.composePageUrl(defaultFile, config.publicServerURL, subdir), params ) : this.pageResponse(path, params, placeholders) @@ -531,16 +529,14 @@ export class PagesRouter extends PromiseRouter { * @param {Object} config The Parse Server configuration. * @returns {Object} The default parameters. */ - async getDefaultParams(config) { - if (!config) { - return {}; - } - const publicServerURL = await config.getPublicServerURL(); - return { - [pageParams.appId]: config.appId, - [pageParams.appName]: config.appName, - [pageParams.publicServerUrl]: publicServerURL, - }; + getDefaultParams(config) { + return config + ? { + [pageParams.appId]: config.appId, + [pageParams.appName]: config.appName, + [pageParams.publicServerUrl]: config.publicServerURL, + } + : {}; } /** diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 4268d39191..2ec993f390 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -88,29 +88,26 @@ export class PublicAPIRouter extends PromiseRouter { ); } - async changePassword(req) { - const config = Config.get(req.query.id); - - if (!config) { - this.invalidRequest(); - } - - if (!config.publicServerURL) { - return { - status: 404, - text: 'Not found.', - }; - } + changePassword(req) { + return new Promise((resolve, reject) => { + const config = Config.get(req.query.id); - const publicServerURL = await config.getPublicServerURL(); + if (!config) { + this.invalidRequest(); + } - // Should we keep the file in memory or leave like that? - return new Promise((resolve, reject) => { + if (!config.publicServerURL) { + return resolve({ + status: 404, + text: 'Not found.', + }); + } + // Should we keep the file in memory or leave like that? fs.readFile(path.resolve(views, 'choose_password'), 'utf-8', (err, data) => { if (err) { return reject(err); } - data = data.replace('PARSE_SERVER_URL', `'${publicServerURL}'`); + data = data.replace('PARSE_SERVER_URL', `'${config.publicServerURL}'`); resolve({ text: data, }); diff --git a/src/batch.js b/src/batch.js index cbd3ff9b90..80fa028cc6 100644 --- a/src/batch.js +++ b/src/batch.js @@ -63,7 +63,7 @@ function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) { // Returns a promise for a {response} object. // TODO: pass along auth correctly -async function handleBatch(router, req) { +function handleBatch(router, req) { if (!Array.isArray(req.body?.requests)) { throw new Parse.Error(Parse.Error.INVALID_JSON, 'requests must be an array'); } @@ -77,11 +77,10 @@ async function handleBatch(router, req) { throw 'internal routing problem - expected url to end with batch'; } - const publicServerURL = await req.config.getPublicServerURL(); const makeRoutablePath = makeBatchRoutingPathFunction( req.originalUrl, req.config.serverURL, - publicServerURL + req.config.publicServerURL ); const batch = transactionRetries => { diff --git a/src/middlewares.js b/src/middlewares.js index 389e3d0ff8..93b16f3846 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -213,25 +213,7 @@ export async function handleParseHeaders(req, res, next) { }); return; } - - // Execute publicServerURL function if it's a function - if (typeof config.publicServerURL === 'function') { - // Store the function for next request and resolve it for this request - const urlFunction = config.publicServerURL; - const resolvedURL = await urlFunction(); - config.publicServerURL = resolvedURL; - // Update the cached config with resolved value - const cachedConfig = AppCache.get(info.appId); - cachedConfig.publicServerURL = resolvedURL; - // But keep the function for next time - cachedConfig._publicServerURLFunction = urlFunction; - } else if (config._publicServerURLFunction) { - // Function was previously stored, execute it again - const resolvedURL = await config._publicServerURLFunction(); - config.publicServerURL = resolvedURL; - const cachedConfig = AppCache.get(info.appId); - cachedConfig.publicServerURL = resolvedURL; - } + await config.loadKeys(); info.app = AppCache.get(info.appId); req.config = config; From 7dabfafadc39fef621b46253fdcda4a0fd60b6a5 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:53:51 +0100 Subject: [PATCH 13/21] fix --- spec/index.spec.js | 2 +- src/Config.js | 5 +++-- src/Routers/UsersRouter.js | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index 7cb006657d..91962252a7 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -686,7 +686,7 @@ describe('server', () => { .catch(done.fail); }); - fdescribe('publicServerURL', () => { + describe('publicServerURL', () => { it('should load publicServerURL', async () => { await reconfigureServer({ publicServerURL: () => 'https://example.com/1', diff --git a/src/Config.js b/src/Config.js index 98344f9ecb..8f0457bf2e 100644 --- a/src/Config.js +++ b/src/Config.js @@ -183,6 +183,7 @@ export class Config { userController, appName, publicServerURL, + _publicServerURL, emailVerifyTokenValidityDuration, emailVerifyTokenReuseIfValid, }) { @@ -191,7 +192,7 @@ export class Config { this.validateEmailConfiguration({ emailAdapter, appName, - publicServerURL, + publicServerURL: publicServerURL || _publicServerURL, emailVerifyTokenValidityDuration, emailVerifyTokenReuseIfValid, }); @@ -474,7 +475,7 @@ export class Config { if (typeof appName !== 'string') { throw 'An app name is required for e-mail verification and password resets.'; } - if (typeof publicServerURL !== 'string') { + if (typeof publicServerURL !== 'string' && typeof publicServerURL !== 'function') { throw 'A public server url is required for e-mail verification and password resets.'; } if (emailVerifyTokenValidityDuration) { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 7668562965..4f38c60b6c 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -418,7 +418,7 @@ export class UsersRouter extends ClassesRouter { Config.validateEmailConfiguration({ emailAdapter: req.config.userController.adapter, appName: req.config.appName, - publicServerURL: req.config.publicServerURL, + publicServerURL: req.config.publicServerURL || req.config._publicServerURL, emailVerifyTokenValidityDuration: req.config.emailVerifyTokenValidityDuration, emailVerifyTokenReuseIfValid: req.config.emailVerifyTokenReuseIfValid, }); From d884a60ed78d48f74f5f73ede9bfb4b1d52dc8af Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:13:16 +0100 Subject: [PATCH 14/21] fix validation --- src/Config.js | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/Config.js b/src/Config.js index 8f0457bf2e..ae375ca086 100644 --- a/src/Config.js +++ b/src/Config.js @@ -140,15 +140,7 @@ export class Config { throw 'extendSessionOnUse must be a boolean value'; } - if (publicServerURL) { - if ( - typeof publicServerURL !== 'function' && - !publicServerURL.startsWith('http://') && - !publicServerURL.startsWith('https://') - ) { - throw 'publicServerURL should be a valid HTTPS URL starting with https://'; - } - } + this.validatePublicServerURL({ publicServerURL }); this.validateSessionConfiguration(sessionLength, expireInactiveSessions); this.validateIps('masterKeyIps', masterKeyIps); this.validateIps('maintenanceKeyIps', maintenanceKeyIps); @@ -462,6 +454,27 @@ export class Config { } } + static validatePublicServerURL({ publicServerURL, required = false }) { + if (!publicServerURL && required) { + throw 'The option publicServerURL is required.'; + } + + const type = typeof publicServerURL; + + if (type === 'string') { + if (!publicServerURL.startsWith('http://') && !publicServerURL.startsWith('https://')) { + throw 'The option publicServerURL must be a valid URL starting with http:// or https://.'; + } + return; + } + + if (type === 'function') { + return; + } + + throw `The option publicServerURL must be a string or function, but got ${type}.`; + } + static validateEmailConfiguration({ emailAdapter, appName, @@ -475,9 +488,7 @@ export class Config { if (typeof appName !== 'string') { throw 'An app name is required for e-mail verification and password resets.'; } - if (typeof publicServerURL !== 'string' && typeof publicServerURL !== 'function') { - throw 'A public server url is required for e-mail verification and password resets.'; - } + this.validatePublicServerURL({ publicServerURL, required: true }); if (emailVerifyTokenValidityDuration) { if (isNaN(emailVerifyTokenValidityDuration)) { throw 'Email verify token validity duration must be a valid number.'; From 2506eda9e826942f625e7ea6e08e2cb714fa3a4b Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:17:10 +0100 Subject: [PATCH 15/21] fix validation --- src/Config.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Config.js b/src/Config.js index ae375ca086..d0a346e5b6 100644 --- a/src/Config.js +++ b/src/Config.js @@ -455,7 +455,10 @@ export class Config { } static validatePublicServerURL({ publicServerURL, required = false }) { - if (!publicServerURL && required) { + if (!publicServerURL) { + if (!required) { + return; + } throw 'The option publicServerURL is required.'; } From b010721c7f51ddd53261ac8a4d0d5871627de157 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:07:22 +0100 Subject: [PATCH 16/21] test --- spec/index.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index 91962252a7..afc1b5362e 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -363,7 +363,7 @@ describe('server', () => { it('should throw when getting invalid mount', done => { reconfigureServer({ publicServerURL: 'blabla:/some' }).catch(error => { - expect(error).toEqual('publicServerURL should be a valid HTTPS URL starting with https://'); + expect(error).toEqual('The option publicServerURL must be a valid URL starting with http:// or https://.'); done(); }); }); From 7f759c85635db58389544441b6072f6477f3d507 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:17:46 +0100 Subject: [PATCH 17/21] fix --- src/Config.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Config.js b/src/Config.js index d0a346e5b6..96a7d7c58f 100644 --- a/src/Config.js +++ b/src/Config.js @@ -32,7 +32,11 @@ function removeTrailingSlash(str) { return str; } +/** + * Config keys that need to be loaded asynchronously. + */ const asyncKeys = ['publicServerURL']; + export class Config { static get(applicationId: string, mount: string) { const cacheInfo = AppCache.get(applicationId); @@ -58,8 +62,6 @@ export class Config { } async loadKeys() { - const asyncKeys = ['publicServerURL']; - await Promise.all( asyncKeys.map(async key => { if (typeof this[`_${key}`] === 'function') { @@ -68,7 +70,14 @@ export class Config { }) ); - AppCache.put(this.appId, this); + const cachedConfig = AppCache.get(this.appId); + if (cachedConfig) { + const updatedConfig = { ...cachedConfig }; + asyncKeys.forEach(key => { + updatedConfig[key] = this[key]; + }); + AppCache.put(this.appId, updatedConfig); + } } static transformConfiguration(serverConfiguration) { From d3d6e2b787aa7d91722de3ab937d15911ddf935e Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:26:14 +0100 Subject: [PATCH 18/21] docs --- src/Options/Definitions.js | 3 ++- src/Options/docs.js | 2 +- src/Options/index.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index d5674eaf29..7baf07e1c6 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -495,7 +495,8 @@ module.exports.ParseServerOptions = { }, publicServerURL: { env: 'PARSE_PUBLIC_SERVER_URL', - help: 'Public URL to your parse server with http:// or https://.', + help: + 'Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL must start with http:// or https://.', }, push: { env: 'PARSE_SERVER_PUSH', diff --git a/src/Options/docs.js b/src/Options/docs.js index 4d268847b1..ea0c2063fc 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -87,7 +87,7 @@ * @property {Boolean} preventLoginWithUnverifiedEmail 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`.
Requires option `verifyUserEmails: true`. * @property {Boolean} preventSignupWithUnverifiedEmail If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.

Default is `false`.
Requires option `verifyUserEmails: true`. * @property {ProtectedFields} protectedFields Protected fields that should be treated with extra security when fetching details. - * @property {String} publicServerURL Public URL to your parse server with http:// or https://. + * @property {String} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL must start with http:// or https://. * @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications * @property {RateLimitOptions[]} rateLimit Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

ℹ️ Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case. * @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes diff --git a/src/Options/index.js b/src/Options/index.js index d5317646ba..f16f2b38b0 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -226,7 +226,7 @@ export interface ParseServerOptions { /* If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`.

ℹ️ The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`. :DEFAULT: true */ encodeParseObjectInCloudFunction: ?boolean; - /* Public URL to your parse server with http:// or https://. + /* Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL must start with http:// or https://. :ENV: PARSE_PUBLIC_SERVER_URL */ publicServerURL: ?string; /* The options for pages such as password reset and email verification. From 14e540667fdb3aa8f4af9a72d83a4268b2cc6bf8 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:34:44 +0100 Subject: [PATCH 19/21] type fix --- src/Options/docs.js | 2 +- src/Options/index.js | 2 +- types/Options/index.d.ts | 2 +- types/ParseServer.d.ts | 5 +++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Options/docs.js b/src/Options/docs.js index ea0c2063fc..64032eaae9 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -87,7 +87,7 @@ * @property {Boolean} preventLoginWithUnverifiedEmail 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`.
Requires option `verifyUserEmails: true`. * @property {Boolean} preventSignupWithUnverifiedEmail If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.

Default is `false`.
Requires option `verifyUserEmails: true`. * @property {ProtectedFields} protectedFields Protected fields that should be treated with extra security when fetching details. - * @property {String} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL must start with http:// or https://. + * @property {Union} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL must start with http:// or https://. * @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications * @property {RateLimitOptions[]} rateLimit Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

ℹ️ Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case. * @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes diff --git a/src/Options/index.js b/src/Options/index.js index f16f2b38b0..1a9cee79ec 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -228,7 +228,7 @@ export interface ParseServerOptions { encodeParseObjectInCloudFunction: ?boolean; /* Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL must start with http:// or https://. :ENV: PARSE_PUBLIC_SERVER_URL */ - publicServerURL: ?string; + publicServerURL: ?(string | (() => string) | (() => Promise)); /* The options for pages such as password reset and email verification. :DEFAULT: {} */ pages: ?PagesOptions; diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index ebe3782696..13fc1bd95d 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -85,7 +85,7 @@ export interface ParseServerOptions { cacheAdapter?: Adapter; emailAdapter?: Adapter; encodeParseObjectInCloudFunction?: boolean; - publicServerURL?: string | (() => string) | Promise; + publicServerURL?: string | (() => string) | (() => Promise); pages?: PagesOptions; customPages?: CustomPagesOptions; liveQuery?: LiveQueryOptions; diff --git a/types/ParseServer.d.ts b/types/ParseServer.d.ts index e504e03114..9570f0cf16 100644 --- a/types/ParseServer.d.ts +++ b/types/ParseServer.d.ts @@ -26,6 +26,11 @@ declare class ParseServer { * @returns {Promise} a promise that resolves when the server is stopped */ handleShutdown(): Promise; + /** + * @static + * Allow developers to customize each request with inversion of control/dependency injection + */ + static applyRequestContextMiddleware(api: any, options: any): void; /** * @static * Create an express app for the parse server From 3511605c718bdbd59ceb8c88dac2ffe7d77447b4 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:39:19 +0100 Subject: [PATCH 20/21] error handling --- src/Config.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Config.js b/src/Config.js index 96a7d7c58f..42b24f2d89 100644 --- a/src/Config.js +++ b/src/Config.js @@ -65,7 +65,11 @@ export class Config { await Promise.all( asyncKeys.map(async key => { if (typeof this[`_${key}`] === 'function') { - this[key] = await this[`_${key}`](); + try { + this[key] = await this[`_${key}`](); + } catch (error) { + throw new Error(`Failed to resolve async config key '${key}': ${error.message}`); + } } }) ); From b508a3a7f0d0af669856d4cac5fff3e20059ca5d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:51:49 +0100 Subject: [PATCH 21/21] docs 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 7baf07e1c6..6c91b1c42c 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -496,7 +496,7 @@ module.exports.ParseServerOptions = { publicServerURL: { env: 'PARSE_PUBLIC_SERVER_URL', help: - 'Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL must start with http:// or https://.', + 'Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`.', }, push: { env: 'PARSE_SERVER_PUSH', diff --git a/src/Options/docs.js b/src/Options/docs.js index 64032eaae9..cbe3efbf39 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -87,7 +87,7 @@ * @property {Boolean} preventLoginWithUnverifiedEmail 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`.
Requires option `verifyUserEmails: true`. * @property {Boolean} preventSignupWithUnverifiedEmail If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.

Default is `false`.
Requires option `verifyUserEmails: true`. * @property {ProtectedFields} protectedFields Protected fields that should be treated with extra security when fetching details. - * @property {Union} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL must start with http:// or https://. + * @property {Union} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`. * @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications * @property {RateLimitOptions[]} rateLimit Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

ℹ️ Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case. * @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes diff --git a/src/Options/index.js b/src/Options/index.js index 1a9cee79ec..54195a8c52 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -226,7 +226,7 @@ export interface ParseServerOptions { /* If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`.

ℹ️ The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`. :DEFAULT: true */ encodeParseObjectInCloudFunction: ?boolean; - /* Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL must start with http:// or https://. + /* Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`. :ENV: PARSE_PUBLIC_SERVER_URL */ publicServerURL: ?(string | (() => string) | (() => Promise)); /* The options for pages such as password reset and email verification.