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:
fengmk2
2025-06-27 17:41:37 +08:00
committed by GitHub
parent 5c45c66ce8
commit dc55518c5b
21 changed files with 214 additions and 43 deletions
@@ -56,13 +56,26 @@ export class CaptchaService {
body: formData,
method: 'POST',
});
const outcome: any = await result.json();
const outcome = (await result.json()) as {
success: boolean;
hostname: string;
};
return (
!!outcome.success &&
// skip hostname check in dev mode
(env.dev || outcome.hostname === this.config.server.host)
if (!outcome.success) return false;
// skip hostname check in dev mode
if (env.dev) return true;
// check if the hostname is in the hosts
if (this.config.server.hosts.includes(outcome.hostname)) return true;
// check if the hostname is in the host
if (this.config.server.host === outcome.hostname) return true;
this.logger.warn(
`Captcha verification failed for hostname: ${outcome.hostname}`
);
return false;
}
private async verifyChallengeResponse(response: any, resource: string) {
@@ -142,7 +142,7 @@ export class OAuthController {
provider: rawState.provider,
})
);
clientUrl.searchParams.set('server', this.url.origin);
clientUrl.searchParams.set('server', this.url.requestOrigin);
return res.redirect(
this.url.link('/open-app/url?', {
@@ -56,7 +56,7 @@ export class WorkerController {
this.logger.error('Invalid Origin', 'ERROR', { origin, referer });
throw new BadRequest('Invalid header');
}
const url = new URL(req.url, this.url.baseUrl);
const url = new URL(req.url, this.url.requestBaseUrl);
const imageURL = url.searchParams.get('url');
if (!imageURL) {
throw new BadRequest('Missing "url" parameter');
@@ -5,7 +5,7 @@ import { fixUrl, OriginRules } from './utils';
@Injectable()
export class WorkerService {
allowedOrigins: OriginRules = [this.url.origin];
allowedOrigins: OriginRules = [...this.url.allowedOrigins];
constructor(
private readonly config: Config,
@@ -18,7 +18,7 @@ export class WorkerService {
...this.config.worker.allowedOrigin
.map(u => fixUrl(u)?.origin as string)
.filter(v => !!v),
this.url.origin,
...this.url.allowedOrigins,
];
}