diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 5880a2ae99..c84534bbc0 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -138,6 +138,7 @@ ] }, "files": [ + "**/__tests__/**/*.spec.ts", "tests/**/*.spec.ts", "tests/**/*.e2e.ts" ], @@ -170,6 +171,7 @@ "*.gen.*" ], "env": { + "AFFINE_SERVER_EXTERNAL_URL": "http://localhost:8080", "TS_NODE_TRANSPILE_ONLY": true, "TS_NODE_PROJECT": "./tsconfig.json", "DEBUG": "affine:*", @@ -187,7 +189,8 @@ "exclude": [ "scripts", "node_modules", - "**/*.spec.ts" + "**/*.spec.ts", + "**/*.e2e.ts" ] }, "stableVersion": "0.5.3", diff --git a/packages/backend/server/src/config/affine.env.ts b/packages/backend/server/src/config/affine.env.ts index 50b383ed90..60bb5fa768 100644 --- a/packages/backend/server/src/config/affine.env.ts +++ b/packages/backend/server/src/config/affine.env.ts @@ -1,5 +1,6 @@ // Convenient way to map environment variables to config values. AFFiNE.ENV_MAP = { + AFFINE_SERVER_EXTERNAL_URL: ['server.externalUrl'], AFFINE_SERVER_PORT: ['server.port', 'int'], AFFINE_SERVER_HOST: 'server.host', AFFINE_SERVER_SUB_PATH: 'server.path', diff --git a/packages/backend/server/src/config/affine.ts b/packages/backend/server/src/config/affine.ts index 0f3748f412..470fb8afb7 100644 --- a/packages/backend/server/src/config/affine.ts +++ b/packages/backend/server/src/config/affine.ts @@ -34,6 +34,9 @@ AFFiNE.server.port = 3010; // /* The sub path of your server */ // /* For example, if you set `AFFiNE.server.path = '/affine'`, then the server will be available at `${domain}/affine` */ // AFFiNE.server.path = '/affine'; +// /* The external URL of your server, will be consist of protocol + host + port by default */ +// /* Useful when you want to customize the link to server resources for example the doc share link or email link */ +// AFFiNE.server.externalUrl = 'http://affine.local:8080' // // // ############################################################### diff --git a/packages/backend/server/src/fundamentals/helpers/__tests__/crypto.spec.ts b/packages/backend/server/src/fundamentals/helpers/__tests__/crypto.spec.ts index 55676d4aab..f46a38a4d7 100644 --- a/packages/backend/server/src/fundamentals/helpers/__tests__/crypto.spec.ts +++ b/packages/backend/server/src/fundamentals/helpers/__tests__/crypto.spec.ts @@ -1,10 +1,8 @@ import { createPrivateKey, createPublicKey } from 'node:crypto'; -import { Test } from '@nestjs/testing'; import ava, { TestFn } from 'ava'; import Sinon from 'sinon'; -import { ConfigModule } from '../../config'; import { CryptoHelper } from '../crypto'; const test = ava as TestFn<{ @@ -39,21 +37,14 @@ const publicKey = createPublicKey({ .toString('utf8'); test.beforeEach(async t => { - const module = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot({ - crypto: { - secret: { - publicKey, - privateKey, - }, - }, - }), - ], - providers: [CryptoHelper], - }).compile(); - - t.context.crypto = module.get(CryptoHelper); + t.context.crypto = new CryptoHelper({ + crypto: { + secret: { + publicKey, + privateKey, + }, + }, + } as any); }); test('should be able to sign and verify', t => { diff --git a/packages/backend/server/src/fundamentals/helpers/__tests__/url.spec.ts b/packages/backend/server/src/fundamentals/helpers/__tests__/url.spec.ts index fc644c6700..d34eb0c178 100644 --- a/packages/backend/server/src/fundamentals/helpers/__tests__/url.spec.ts +++ b/packages/backend/server/src/fundamentals/helpers/__tests__/url.spec.ts @@ -1,8 +1,6 @@ -import { Test } from '@nestjs/testing'; import ava, { TestFn } from 'ava'; import Sinon from 'sinon'; -import { ConfigModule } from '../../config'; import { URLHelper } from '../url'; const test = ava as TestFn<{ @@ -10,24 +8,60 @@ const test = ava as TestFn<{ }>; test.beforeEach(async t => { - const module = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot({ - server: { - host: 'app.affine.local', - port: 3010, - https: true, - }, - }), - ], - providers: [URLHelper], - }).compile(); - - t.context.url = module.get(URLHelper); + t.context.url = new URLHelper({ + server: { + externalUrl: '', + host: 'app.affine.local', + port: 3010, + https: true, + path: '', + }, + } as any); }); -test('can get home page', t => { - t.is(t.context.url.home, 'https://app.affine.local'); +test('can factor base url correctly without specified external url', t => { + t.is(t.context.url.baseUrl, 'https://app.affine.local'); +}); + +test('can factor base url correctly with specified external url', t => { + const url = new URLHelper({ + server: { + externalUrl: 'https://external.domain.com', + host: 'app.affine.local', + port: 3010, + https: true, + path: '/ignored', + }, + } as any); + + t.is(url.baseUrl, 'https://external.domain.com'); +}); + +test('can factor base url correctly with specified external url and path', t => { + const url = new URLHelper({ + server: { + externalUrl: 'https://external.domain.com/anything', + host: 'app.affine.local', + port: 3010, + https: true, + path: '/ignored', + }, + } as any); + + t.is(url.baseUrl, 'https://external.domain.com/anything'); +}); + +test('can factor base url correctly with specified external url with port', t => { + const url = new URLHelper({ + server: { + externalUrl: 'https://external.domain.com:123', + host: 'app.affine.local', + port: 3010, + https: true, + }, + } as any); + + t.is(url.baseUrl, 'https://external.domain.com:123'); }); test('can stringify query', t => { diff --git a/packages/backend/server/src/fundamentals/helpers/url.ts b/packages/backend/server/src/fundamentals/helpers/url.ts index e460c9a0d3..3869b6ebe5 100644 --- a/packages/backend/server/src/fundamentals/helpers/url.ts +++ b/packages/backend/server/src/fundamentals/helpers/url.ts @@ -1,3 +1,5 @@ +import { isIP } from 'node:net'; + import { Injectable } from '@nestjs/common'; import type { Response } from 'express'; @@ -6,19 +8,37 @@ import { Config } from '../config'; @Injectable() export class URLHelper { private readonly redirectAllowHosts: string[]; - readonly origin = this.config.node.dev - ? 'http://localhost:8080' - : `${this.config.server.https ? 'https' : 'http'}://${this.config.server.host}${ - this.config.server.host === 'localhost' || - this.config.server.host === '0.0.0.0' - ? `:${this.config.server.port}` - : '' - }`; - readonly baseUrl = `${this.origin}${this.config.server.path}`; - readonly home = this.baseUrl; + readonly origin: string; + readonly baseUrl: string; + readonly home: string; constructor(private readonly config: Config) { + if (this.config.server.externalUrl) { + if (!this.verify(this.config.server.externalUrl)) { + throw new Error( + 'Invalid `server.externalUrl` configured. It must be a valid url.' + ); + } + + const externalUrl = new URL(this.config.server.externalUrl); + + this.origin = externalUrl.origin; + this.baseUrl = + externalUrl.origin + externalUrl.pathname.replace(/\/$/, ''); + } else { + this.origin = [ + this.config.server.https ? 'https' : 'http', + '://', + this.config.server.host, + this.config.server.host === 'localhost' || isIP(this.config.server.host) + ? `:${this.config.server.port}` + : '', + ].join(''); + this.baseUrl = this.origin + this.config.server.path; + } + + this.home = this.baseUrl; this.redirectAllowHosts = [this.baseUrl]; } diff --git a/packages/backend/server/src/fundamentals/nestjs/config.ts b/packages/backend/server/src/fundamentals/nestjs/config.ts index 80e888eadc..eb3b210d9d 100644 --- a/packages/backend/server/src/fundamentals/nestjs/config.ts +++ b/packages/backend/server/src/fundamentals/nestjs/config.ts @@ -1,29 +1,25 @@ import { defineStartupConfig, ModuleConfig } from '../../fundamentals/config'; export interface ServerStartupConfigurations { + /** + * Base url of AFFiNE server, used for generating external urls. + * default to be `[AFFiNE.protocol]://[AFFiNE.host][:AFFiNE.port]?[AFFiNE.path]` if not specified + */ + externalUrl: string; /** * Whether the server is hosted on a ssl enabled domain */ https: boolean; /** - * where the server get deployed. - * - * @default 'localhost' - * @env AFFINE_SERVER_HOST + * where the server get deployed(FQDN). */ host: string; /** * which port the server will listen on - * - * @default 3010 - * @env AFFINE_SERVER_PORT */ port: number; /** * subpath where the server get deployed if there is. - * - * @default '' // empty string - * @env AFFINE_SERVER_SUB_PATH */ path: string; } @@ -35,6 +31,7 @@ declare module '../../fundamentals/config' { } defineStartupConfig('server', { + externalUrl: '', https: false, host: 'localhost', port: 3010,