mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-03 18:40:00 +08:00
feat(server): allow customize server external url (#7270)
closes https://github.com/toeverything/AFFiNE/issues/7252
This commit is contained in:
@@ -138,6 +138,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
"**/__tests__/**/*.spec.ts",
|
||||||
"tests/**/*.spec.ts",
|
"tests/**/*.spec.ts",
|
||||||
"tests/**/*.e2e.ts"
|
"tests/**/*.e2e.ts"
|
||||||
],
|
],
|
||||||
@@ -170,6 +171,7 @@
|
|||||||
"*.gen.*"
|
"*.gen.*"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
|
"AFFINE_SERVER_EXTERNAL_URL": "http://localhost:8080",
|
||||||
"TS_NODE_TRANSPILE_ONLY": true,
|
"TS_NODE_TRANSPILE_ONLY": true,
|
||||||
"TS_NODE_PROJECT": "./tsconfig.json",
|
"TS_NODE_PROJECT": "./tsconfig.json",
|
||||||
"DEBUG": "affine:*",
|
"DEBUG": "affine:*",
|
||||||
@@ -187,7 +189,8 @@
|
|||||||
"exclude": [
|
"exclude": [
|
||||||
"scripts",
|
"scripts",
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"**/*.spec.ts"
|
"**/*.spec.ts",
|
||||||
|
"**/*.e2e.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"stableVersion": "0.5.3",
|
"stableVersion": "0.5.3",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Convenient way to map environment variables to config values.
|
// Convenient way to map environment variables to config values.
|
||||||
AFFiNE.ENV_MAP = {
|
AFFiNE.ENV_MAP = {
|
||||||
|
AFFINE_SERVER_EXTERNAL_URL: ['server.externalUrl'],
|
||||||
AFFINE_SERVER_PORT: ['server.port', 'int'],
|
AFFINE_SERVER_PORT: ['server.port', 'int'],
|
||||||
AFFINE_SERVER_HOST: 'server.host',
|
AFFINE_SERVER_HOST: 'server.host',
|
||||||
AFFINE_SERVER_SUB_PATH: 'server.path',
|
AFFINE_SERVER_SUB_PATH: 'server.path',
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ AFFiNE.server.port = 3010;
|
|||||||
// /* The sub path of your server */
|
// /* The sub path of your server */
|
||||||
// /* For example, if you set `AFFiNE.server.path = '/affine'`, then the server will be available at `${domain}/affine` */
|
// /* For example, if you set `AFFiNE.server.path = '/affine'`, then the server will be available at `${domain}/affine` */
|
||||||
// AFFiNE.server.path = '/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'
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
// ###############################################################
|
// ###############################################################
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { createPrivateKey, createPublicKey } from 'node:crypto';
|
import { createPrivateKey, createPublicKey } from 'node:crypto';
|
||||||
|
|
||||||
import { Test } from '@nestjs/testing';
|
|
||||||
import ava, { TestFn } from 'ava';
|
import ava, { TestFn } from 'ava';
|
||||||
import Sinon from 'sinon';
|
import Sinon from 'sinon';
|
||||||
|
|
||||||
import { ConfigModule } from '../../config';
|
|
||||||
import { CryptoHelper } from '../crypto';
|
import { CryptoHelper } from '../crypto';
|
||||||
|
|
||||||
const test = ava as TestFn<{
|
const test = ava as TestFn<{
|
||||||
@@ -39,21 +37,14 @@ const publicKey = createPublicKey({
|
|||||||
.toString('utf8');
|
.toString('utf8');
|
||||||
|
|
||||||
test.beforeEach(async t => {
|
test.beforeEach(async t => {
|
||||||
const module = await Test.createTestingModule({
|
t.context.crypto = new CryptoHelper({
|
||||||
imports: [
|
crypto: {
|
||||||
ConfigModule.forRoot({
|
secret: {
|
||||||
crypto: {
|
publicKey,
|
||||||
secret: {
|
privateKey,
|
||||||
publicKey,
|
},
|
||||||
privateKey,
|
},
|
||||||
},
|
} as any);
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
providers: [CryptoHelper],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
t.context.crypto = module.get(CryptoHelper);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should be able to sign and verify', t => {
|
test('should be able to sign and verify', t => {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Test } from '@nestjs/testing';
|
|
||||||
import ava, { TestFn } from 'ava';
|
import ava, { TestFn } from 'ava';
|
||||||
import Sinon from 'sinon';
|
import Sinon from 'sinon';
|
||||||
|
|
||||||
import { ConfigModule } from '../../config';
|
|
||||||
import { URLHelper } from '../url';
|
import { URLHelper } from '../url';
|
||||||
|
|
||||||
const test = ava as TestFn<{
|
const test = ava as TestFn<{
|
||||||
@@ -10,24 +8,60 @@ const test = ava as TestFn<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
test.beforeEach(async t => {
|
test.beforeEach(async t => {
|
||||||
const module = await Test.createTestingModule({
|
t.context.url = new URLHelper({
|
||||||
imports: [
|
server: {
|
||||||
ConfigModule.forRoot({
|
externalUrl: '',
|
||||||
server: {
|
host: 'app.affine.local',
|
||||||
host: 'app.affine.local',
|
port: 3010,
|
||||||
port: 3010,
|
https: true,
|
||||||
https: true,
|
path: '',
|
||||||
},
|
},
|
||||||
}),
|
} as any);
|
||||||
],
|
|
||||||
providers: [URLHelper],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
t.context.url = module.get(URLHelper);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can get home page', t => {
|
test('can factor base url correctly without specified external url', t => {
|
||||||
t.is(t.context.url.home, 'https://app.affine.local');
|
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 => {
|
test('can stringify query', t => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isIP } from 'node:net';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import type { Response } from 'express';
|
import type { Response } from 'express';
|
||||||
|
|
||||||
@@ -6,19 +8,37 @@ import { Config } from '../config';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class URLHelper {
|
export class URLHelper {
|
||||||
private readonly redirectAllowHosts: string[];
|
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 origin: string;
|
||||||
readonly home = this.baseUrl;
|
readonly baseUrl: string;
|
||||||
|
readonly home: string;
|
||||||
|
|
||||||
constructor(private readonly config: Config) {
|
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];
|
this.redirectAllowHosts = [this.baseUrl];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,25 @@
|
|||||||
import { defineStartupConfig, ModuleConfig } from '../../fundamentals/config';
|
import { defineStartupConfig, ModuleConfig } from '../../fundamentals/config';
|
||||||
|
|
||||||
export interface ServerStartupConfigurations {
|
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
|
* Whether the server is hosted on a ssl enabled domain
|
||||||
*/
|
*/
|
||||||
https: boolean;
|
https: boolean;
|
||||||
/**
|
/**
|
||||||
* where the server get deployed.
|
* where the server get deployed(FQDN).
|
||||||
*
|
|
||||||
* @default 'localhost'
|
|
||||||
* @env AFFINE_SERVER_HOST
|
|
||||||
*/
|
*/
|
||||||
host: string;
|
host: string;
|
||||||
/**
|
/**
|
||||||
* which port the server will listen on
|
* which port the server will listen on
|
||||||
*
|
|
||||||
* @default 3010
|
|
||||||
* @env AFFINE_SERVER_PORT
|
|
||||||
*/
|
*/
|
||||||
port: number;
|
port: number;
|
||||||
/**
|
/**
|
||||||
* subpath where the server get deployed if there is.
|
* subpath where the server get deployed if there is.
|
||||||
*
|
|
||||||
* @default '' // empty string
|
|
||||||
* @env AFFINE_SERVER_SUB_PATH
|
|
||||||
*/
|
*/
|
||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
@@ -35,6 +31,7 @@ declare module '../../fundamentals/config' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defineStartupConfig('server', {
|
defineStartupConfig('server', {
|
||||||
|
externalUrl: '',
|
||||||
https: false,
|
https: false,
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3010,
|
port: 3010,
|
||||||
|
|||||||
Reference in New Issue
Block a user