feat: add configurable API server URLs and CORS settings for development

Adds environment variables to configure API server URLs and CORS settings
for development environments, enabling flexible multi-server setups and
cross-origin testing.
This commit is contained in:
midzelis
2025-10-22 22:17:05 +00:00
parent fc5fc58759
commit 6fd39767db
7 changed files with 115 additions and 2 deletions

19
pnpm-lock.yaml generated
View File

@@ -789,6 +789,9 @@ importers:
'@koddsson/eslint-plugin-tscompat':
specifier: ^0.2.0
version: 0.2.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)
'@rollup/plugin-replace':
specifier: ^6.0.2
version: 6.0.2(rollup@4.52.5)
'@socket.io/component-emitter':
specifier: ^3.1.0
version: 3.1.2
@@ -3681,6 +3684,15 @@ packages:
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@rollup/plugin-replace@6.0.2':
resolution: {integrity: sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
@@ -15290,6 +15302,13 @@ snapshots:
dependencies:
react: 19.2.0
'@rollup/plugin-replace@6.0.2(rollup@4.52.5)':
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
magic-string: 0.30.19
optionalDependencies:
rollup: 4.52.5
'@rollup/pluginutils@5.3.0(rollup@4.52.5)':
dependencies:
'@types/estree': 1.0.8

View File

@@ -195,4 +195,10 @@ export class EnvDto {
@IsString()
@Optional()
REDIS_URL?: string;
@ValidateBoolean({ optional: true })
IMMICH_DEV_CORS_ALL_ORIGINS?: boolean;
@ValidateBoolean({ optional: true })
IMMICH_DEV_CORS_CREDENTIALS?: boolean;
}

View File

@@ -1,5 +1,6 @@
import { RegisterQueueOptions } from '@nestjs/bullmq';
import { Inject, Injectable, Optional } from '@nestjs/common';
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
import { QueueOptions } from 'bullmq';
import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator';
@@ -30,6 +31,13 @@ export interface EnvData {
configFile?: string;
logLevel?: LogLevel;
dev: {
cors: {
allOrigins?: boolean;
credentials?: boolean;
};
};
buildMetadata: {
build?: string;
buildUrl?: string;
@@ -222,6 +230,13 @@ const getEnv = (): EnvData => {
configFile: dto.IMMICH_CONFIG_FILE,
logLevel: dto.IMMICH_LOG_LEVEL,
dev: {
cors: {
allOrigins: dto.IMMICH_DEV_CORS_ALL_ORIGINS,
credentials: dto.IMMICH_DEV_CORS_CREDENTIALS,
},
},
buildMetadata: {
build: dto.IMMICH_BUILD,
buildUrl: dto.IMMICH_BUILD_URL,
@@ -342,6 +357,24 @@ export class ConfigRepository {
return this.getEnv().environment === ImmichEnvironment.Development;
}
getCorsOptions(): CorsOptions | undefined {
const options: Partial<CorsOptions> = {};
const env = this.getEnv();
if (env.dev.cors.allOrigins) {
options.origin = (requestOrigin, callback) => {
callback(null, requestOrigin);
};
}
if (env.dev.cors.credentials) {
options.credentials = env.dev.cors.credentials;
}
if (Object.keys(options).length > 0) {
return options;
}
return undefined;
}
getWorker() {
return this.worker;
}

View File

@@ -34,7 +34,17 @@ async function bootstrap() {
app.use(cookieParser());
app.use(json({ limit: '10mb' }));
if (configRepository.isDev()) {
app.enableCors();
const options = configRepository.getCorsOptions();
if (options) {
logger.warn(`Enabling CORS: ${JSON.stringify(configRepository.getEnv().dev.cors)}`);
logger.warn(
'NOTE: to properly support a fully statically hosted frontend you MUST configure the frontend/backend to be on the same site. i.e. frontend=https://localhost:1234 and backend=http://localhost:2283 or configure TLS',
);
app.enableCors(options);
} else {
logger.warn('Enabling CORS');
app.enableCors();
}
}
app.useWebSocketAdapter(new WebSocketAdapter(app));
useSwagger(app, { write: configRepository.isDev() });

View File

@@ -65,6 +65,7 @@
"@eslint/js": "^9.36.0",
"@faker-js/faker": "^10.0.0",
"@koddsson/eslint-plugin-tscompat": "^0.2.0",
"@rollup/plugin-replace": "^6.0.2",
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.8.0",

View File

@@ -1,15 +1,48 @@
import { retrieveServerConfig } from '$lib/stores/server-config.store';
import { initLanguage } from '$lib/utils';
import { AbortError, initLanguage, sleep } from '$lib/utils';
import { defaults } from '@immich/sdk';
import { memoize } from 'lodash-es';
type Fetch = typeof fetch;
const api_server: string = '@IMMICH_API_SERVER@';
const tryServers = async (fetchFn: typeof fetch) => {
const servers = api_server
.split(',')
.map((v) => v.trim())
.filter((v) => v !== '');
if (servers.length === 0) {
return true;
}
// servers are in priority order, try in parallel, use first success
const fetchers = servers.map(async (url: string) => {
const response = await fetchFn(url + '/server/config');
if (response.ok) {
return url;
}
throw new AbortError();
});
try {
const urlWinner = await Promise.any(fetchers);
defaults.baseUrl = urlWinner;
defaults.fetch = (url, options) => fetchFn(url, { credentials: 'include', ...options });
} catch (e) {
console.error(e);
return false;
}
};
async function _init(fetch: Fetch) {
// set event.fetch on the fetch-client used by @immich/sdk
// https://kit.svelte.dev/docs/load#making-fetch-requests
// https://github.com/oazapfts/oazapfts/blob/main/README.md#fetch-options
defaults.fetch = fetch;
try {
await Promise.race([tryServers(fetch), sleep(5000)]);
} catch {
throw 'Could not connect to any server';
}
await initLanguage();
await retrieveServerConfig();
}

View File

@@ -1,3 +1,4 @@
import replace from '@rollup/plugin-replace';
import { enhancedImages } from '@sveltejs/enhanced-img';
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
@@ -39,6 +40,16 @@ export default defineConfig({
enhancedImages(),
tailwindcss(),
sveltekit(),
replace({
preventAssignment: true,
include: ['**/server.ts'],
sourceMap: true,
objectGuards: false,
delimiters: ['@', '@'],
values: {
IMMICH_API_SERVER: process.env.IMMICH_API_SERVER ?? '',
},
}),
process.env.BUILD_STATS === 'true'
? visualizer({
emitFile: true,