feat(server): allow customize server external url (#7270)

closes https://github.com/toeverything/AFFiNE/issues/7252
This commit is contained in:
forehalo 2024-06-19 12:03:36 +00:00
parent 9746ddb5e0
commit d34c5c42ef
No known key found for this signature in database
GPG Key ID: 56709255DC7EC728
7 changed files with 105 additions and 56 deletions

View File

@ -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",

View File

@ -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',

View File

@ -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'
//
//
// ###############################################################

View File

@ -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 => {

View File

@ -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 => {

View File

@ -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];
}

View File

@ -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,