mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 02:56:23 +08:00
feat(server): support multiple hosts in one deployment (#12950)
close CLOUD-233 #### PR Dependency Tree * **PR #12950** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for configuring multiple server hosts across backend and frontend settings. * Enhanced deployment and Helm chart configuration to allow specifying multiple ingress hosts. * Updated admin and configuration interfaces to display and manage multiple server hosts. * **Improvements** * Improved URL generation, OAuth, and worker service logic to dynamically handle requests from multiple hosts. * Enhanced captcha verification to support multiple allowed hostnames. * Updated frontend logic for platform-specific server base URLs and allowed origins, including Apple app domains. * Expanded test coverage for multi-host scenarios. * **Bug Fixes** * Corrected backend logic to consistently use dynamic base URLs and origins based on request host context. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -12,6 +12,7 @@ test.beforeEach(async t => {
|
||||
server: {
|
||||
externalUrl: '',
|
||||
host: 'app.affine.local',
|
||||
hosts: [],
|
||||
port: 3010,
|
||||
https: true,
|
||||
path: '',
|
||||
@@ -28,6 +29,7 @@ test('can factor base url correctly with specified external url', t => {
|
||||
server: {
|
||||
externalUrl: 'https://external.domain.com',
|
||||
host: 'app.affine.local',
|
||||
hosts: [],
|
||||
port: 3010,
|
||||
https: true,
|
||||
path: '/ignored',
|
||||
@@ -42,6 +44,7 @@ test('can factor base url correctly with specified external url and path', t =>
|
||||
server: {
|
||||
externalUrl: 'https://external.domain.com/anything',
|
||||
host: 'app.affine.local',
|
||||
hosts: [],
|
||||
port: 3010,
|
||||
https: true,
|
||||
path: '/ignored',
|
||||
@@ -56,6 +59,7 @@ test('can factor base url correctly with specified external url with port', t =>
|
||||
server: {
|
||||
externalUrl: 'https://external.domain.com:123',
|
||||
host: 'app.affine.local',
|
||||
hosts: [],
|
||||
port: 3010,
|
||||
https: true,
|
||||
},
|
||||
@@ -95,7 +99,7 @@ test('can safe redirect', t => {
|
||||
|
||||
function deny(to: string) {
|
||||
t.context.url.safeRedirect(res, to);
|
||||
t.true(spy.calledOnceWith(t.context.url.home));
|
||||
t.true(spy.calledOnceWith(t.context.url.baseUrl));
|
||||
spy.resetHistory();
|
||||
}
|
||||
|
||||
@@ -106,3 +110,38 @@ test('can safe redirect', t => {
|
||||
].forEach(allow);
|
||||
['https://other.domain.com', 'a://invalid.uri'].forEach(deny);
|
||||
});
|
||||
|
||||
test('can get request origin', t => {
|
||||
t.is(t.context.url.requestOrigin, 'https://app.affine.local');
|
||||
});
|
||||
|
||||
test('can get request base url', t => {
|
||||
t.is(t.context.url.requestBaseUrl, 'https://app.affine.local');
|
||||
});
|
||||
|
||||
test('can get request base url with multiple hosts', t => {
|
||||
// mock cls
|
||||
const cls = new Map<string, string>();
|
||||
const url = new URLHelper(
|
||||
{
|
||||
server: {
|
||||
externalUrl: '',
|
||||
host: 'app.affine.local1',
|
||||
hosts: ['app.affine.local1', 'app.affine.local2'],
|
||||
port: 3010,
|
||||
https: true,
|
||||
path: '',
|
||||
},
|
||||
} as any,
|
||||
cls as any
|
||||
);
|
||||
|
||||
// no cls, use default origin
|
||||
t.is(url.requestOrigin, 'https://app.affine.local1');
|
||||
t.is(url.requestBaseUrl, 'https://app.affine.local1');
|
||||
|
||||
// set cls
|
||||
cls.set(CLS_REQUEST_HOST, 'app.affine.local2');
|
||||
t.is(url.requestOrigin, 'https://app.affine.local2');
|
||||
t.is(url.requestBaseUrl, 'https://app.affine.local2');
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { isIP } from 'node:net';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
|
||||
import { Config } from '../config';
|
||||
import { OnEvent } from '../event';
|
||||
@@ -11,10 +12,13 @@ export class URLHelper {
|
||||
redirectAllowHosts!: string[];
|
||||
|
||||
origin!: string;
|
||||
allowedOrigins!: string[];
|
||||
baseUrl!: string;
|
||||
home!: string;
|
||||
|
||||
constructor(private readonly config: Config) {
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly cls?: ClsService
|
||||
) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
@@ -34,19 +38,40 @@ export class URLHelper {
|
||||
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.origin = this.convertHostToOrigin(this.config.server.host);
|
||||
this.baseUrl = this.origin + this.config.server.path;
|
||||
}
|
||||
|
||||
this.home = this.baseUrl;
|
||||
this.redirectAllowHosts = [this.baseUrl];
|
||||
|
||||
this.allowedOrigins = [this.origin];
|
||||
if (this.config.server.hosts.length > 0) {
|
||||
for (const host of this.config.server.hosts) {
|
||||
this.allowedOrigins.push(this.convertHostToOrigin(host));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get requestOrigin() {
|
||||
if (this.config.server.hosts.length === 0) {
|
||||
return this.origin;
|
||||
}
|
||||
|
||||
// support multiple hosts
|
||||
const requestHost = this.cls?.get<string | undefined>(CLS_REQUEST_HOST);
|
||||
if (!requestHost || !this.config.server.hosts.includes(requestHost)) {
|
||||
return this.origin;
|
||||
}
|
||||
|
||||
return this.convertHostToOrigin(requestHost);
|
||||
}
|
||||
|
||||
get requestBaseUrl() {
|
||||
if (this.config.server.hosts.length === 0) {
|
||||
return this.baseUrl;
|
||||
}
|
||||
|
||||
return this.requestOrigin + this.config.server.path;
|
||||
}
|
||||
|
||||
stringify(query: Record<string, any>) {
|
||||
@@ -72,7 +97,7 @@ export class URLHelper {
|
||||
}
|
||||
|
||||
url(path: string, query: Record<string, any> = {}) {
|
||||
const url = new URL(path, this.origin);
|
||||
const url = new URL(path, this.requestOrigin);
|
||||
|
||||
for (const key in query) {
|
||||
url.searchParams.set(key, query[key]);
|
||||
@@ -87,7 +112,7 @@ export class URLHelper {
|
||||
|
||||
safeRedirect(res: Response, to: string) {
|
||||
try {
|
||||
const finalTo = new URL(decodeURIComponent(to), this.baseUrl);
|
||||
const finalTo = new URL(decodeURIComponent(to), this.requestBaseUrl);
|
||||
|
||||
for (const host of this.redirectAllowHosts) {
|
||||
const hostURL = new URL(host);
|
||||
@@ -103,7 +128,7 @@ export class URLHelper {
|
||||
}
|
||||
|
||||
// redirect to home if the url is invalid
|
||||
return res.redirect(this.home);
|
||||
return res.redirect(this.baseUrl);
|
||||
}
|
||||
|
||||
verify(url: string | URL) {
|
||||
@@ -118,4 +143,13 @@ export class URLHelper {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private convertHostToOrigin(host: string) {
|
||||
return [
|
||||
this.config.server.https ? 'https' : 'http',
|
||||
'://',
|
||||
host,
|
||||
host === 'localhost' || isIP(host) ? `:${this.config.server.port}` : '',
|
||||
].join('');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user