diff --git a/.env.example b/.env.example index e5820a1..2e952fe 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,7 @@ PROVIDER_BASE_URL_ALLOWLIST=[] PUBLIC_BASE_URL=http://localhost:8000 CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"] VITE_API_BASE= +VITE_ALLOWED_HOSTS= # Optional frontend build network and npm fetch tuning: DOCKER_BUILD_NETWORK=default @@ -61,3 +62,4 @@ NPM_FETCH_TIMEOUT=300000 # PUBLIC_BASE_URL=https://api.example.com # CORS_ORIGINS=["https://app.example.com"] # VITE_API_BASE=https://api.example.com/api/v1 +# VITE_ALLOWED_HOSTS=app.example.com diff --git a/doc/README.md b/doc/README.md index 647ad8a..2b4935d 100644 --- a/doc/README.md +++ b/doc/README.md @@ -8,6 +8,6 @@ This directory contains technical documentation for DMS. - `architecture-overview.md` - backend, frontend, and infrastructure architecture - `api-contract.md` - API endpoint contract grouped by route module, including session auth, login throttle responses, role and ownership scope, upload limits, and settings or processing-log security constraints - `data-model-reference.md` - database entity definitions and lifecycle states -- `operations-and-configuration.md` - runtime operations, hardened compose defaults, DEV and LIVE security values, and persisted settings configuration behavior +- `operations-and-configuration.md` - runtime operations, hardened compose defaults, DEV and LIVE security values, persisted settings configuration behavior, and frontend Vite host allowlist controls - `frontend-design-foundation.md` - frontend visual system, tokens, UI implementation rules, authenticated media delivery under session auth, processing-log timeline behavior, and settings helper-copy guidance - `../.env.example` - repository-level environment template with local defaults and production override guidance diff --git a/doc/operations-and-configuration.md b/doc/operations-and-configuration.md index 7f5912c..f209f37 100644 --- a/doc/operations-and-configuration.md +++ b/doc/operations-and-configuration.md @@ -87,6 +87,7 @@ Use `.env.example` as baseline. The table below documents user-managed settings | `HOST_BIND_IP` | `127.0.0.1` or local LAN bind if needed | `127.0.0.1` (publish behind proxy only) | | `PUBLIC_BASE_URL` | `http://localhost:8000` | `https://api.example.com` | | `VITE_API_BASE` | empty for host-derived `http://:8000/api/v1`, or explicit local URL | `https://api.example.com/api/v1` | +| `VITE_ALLOWED_HOSTS` | optional comma-separated hostnames, for example `localhost,docs.lan` | optional comma-separated public frontend hostnames, for example `app.example.com` | | `CORS_ORIGINS` | `["http://localhost:5173","http://localhost:3000"]` | exact frontend origins only, for example `["https://app.example.com"]` | | `REDIS_URL` | `redis://:@redis:6379/0` in isolated local network | `rediss://:@redis.internal:6379/0` | | `REDIS_SECURITY_MODE` | `compat` or `auto` | `strict` | @@ -138,6 +139,9 @@ Recommended LIVE pattern: ## Frontend Runtime - Frontend no longer consumes `VITE_API_TOKEN`. +- Vite dev server host allowlist uses the union of: + - hostnames extracted from `CORS_ORIGINS` + - optional explicit hostnames from `VITE_ALLOWED_HOSTS` - Session authentication is cookie-based; browser reloads and new tabs can reuse an active session until it expires or is revoked. - Protected media and file download flows still use authenticated fetch plus blob/object URL handling. diff --git a/docker-compose.yml b/docker-compose.yml index a6b4768..621faba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -152,6 +152,8 @@ services: NPM_FETCH_TIMEOUT: ${NPM_FETCH_TIMEOUT:-300000} environment: VITE_API_BASE: ${VITE_API_BASE:-} + CORS_ORIGINS: '${CORS_ORIGINS:-["http://localhost:5173","http://localhost:3000"]}' + VITE_ALLOWED_HOSTS: ${VITE_ALLOWED_HOSTS:-} # ports: # - "${HOST_BIND_IP:-127.0.0.1}:5173:5173" volumes: diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 5c6934f..7f50d21 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,14 +1,85 @@ /** * Vite configuration for the DMS frontend application. */ -import { defineConfig } from 'vite'; +import { defineConfig, loadEnv } from 'vite'; + +/** + * Parses a comma-separated environment value into normalized entries. + * + * @param rawValue Raw comma-separated value. + * @returns List of non-empty normalized entries. + */ +function parseCsvList(rawValue: string | undefined): string[] { + if (!rawValue) { + return []; + } + + return rawValue + .split(',') + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0); +} + +/** + * Extracts hostnames from CORS origin values. + * + * @param rawValue JSON array string or comma-separated origin list. + * @returns Hostnames parsed from valid origins. + */ +function parseCorsOriginHosts(rawValue: string | undefined): string[] { + if (!rawValue) { + return []; + } + + let origins: string[] = []; + + try { + const parsedOrigins = JSON.parse(rawValue); + if (Array.isArray(parsedOrigins)) { + origins = parsedOrigins.filter((entry): entry is string => typeof entry === 'string'); + } else if (typeof parsedOrigins === 'string') { + origins = [parsedOrigins]; + } + } catch { + origins = parseCsvList(rawValue); + } + + return origins.flatMap((origin) => { + try { + const parsedUrl = new URL(origin); + return parsedUrl.hostname ? [parsedUrl.hostname] : []; + } catch { + return []; + } + }); +} + +/** + * Builds the Vite allowed host list from environment-driven inputs. + * + * @param env Environment variable key-value map. + * @returns De-duplicated hostnames, or undefined to keep Vite defaults. + */ +function buildAllowedHosts(env: Record): string[] | undefined { + const explicitHosts = parseCsvList(env.VITE_ALLOWED_HOSTS); + const corsOriginHosts = parseCorsOriginHosts(env.CORS_ORIGINS); + const mergedHosts = Array.from(new Set([...explicitHosts, ...corsOriginHosts])); + + return mergedHosts.length > 0 ? mergedHosts : undefined; +} /** * Exports frontend build and dev-server settings. */ -export default defineConfig({ - server: { - host: '0.0.0.0', - port: 5173, - }, +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + const allowedHosts = buildAllowedHosts(env); + + return { + server: { + host: '0.0.0.0', + port: 5173, + ...(allowedHosts ? { allowedHosts } : {}), + }, + }; });