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
7 changed files with 105 additions and 56 deletions
@@ -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 => {
@@ -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 => {
@@ -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];
}
@@ -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,