diff --git a/README.md b/README.md index 4a82c96..5f89a80 100644 --- a/README.md +++ b/README.md @@ -116,9 +116,9 @@ cd frontend && npm run preview Main runtime variables are defined in `docker-compose.yml`: - API and worker: `DATABASE_URL`, `REDIS_URL`, `REDIS_SECURITY_MODE`, `REDIS_TLS_MODE`, `STORAGE_ROOT`, `PUBLIC_BASE_URL`, `CORS_ORIGINS`, `ALLOW_DEVELOPMENT_ANONYMOUS_USER_ACCESS`, `TYPESENSE_*`, `APP_SETTINGS_ENCRYPTION_KEY` -- Frontend: optional `VITE_API_BASE`, optional `VITE_API_TOKEN` compatibility fallback, optional `VITE_DEV_PROXY_TARGET` +- Frontend: optional `VITE_API_BASE`, optional `VITE_API_TOKEN` compatibility fallback -When `VITE_API_BASE` is unset, the frontend uses relative `/api/v1` paths via Vite proxy. +When `VITE_API_BASE` is unset, the frontend uses `http://:8000/api/v1`. Application settings saved from the UI persist at: diff --git a/backend/app/main.py b/backend/app/main.py index 1539c0b..b4a79fa 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -18,16 +18,7 @@ from app.services.typesense_index import ensure_typesense_collection settings = get_settings() UPLOAD_ENDPOINT_PATH = "/api/v1/documents/upload" UPLOAD_ENDPOINT_METHOD = "POST" -DEVELOPMENT_CORS_PRIVATE_ORIGIN_REGEX = ( - r"^https?://(" - r"localhost" - r"|127(?:\.\d{1,3}){3}" - r"|0\.0\.0\.0" - r"|10(?:\.\d{1,3}){3}" - r"|192\.168(?:\.\d{1,3}){2}" - r"|172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2}" - r")(?::\d+)?$" -) +CORS_HTTP_ORIGIN_REGEX = r"^https?://[^/]+$" def _is_upload_size_guard_target(request: Request) -> bool: @@ -44,14 +35,10 @@ def create_app() -> FastAPI: """Builds and configures the FastAPI application instance.""" app = FastAPI(title="DCM DMS API", version="0.1.0") - app_env = settings.app_env.strip().lower() - allow_origin_regex = ( - DEVELOPMENT_CORS_PRIVATE_ORIGIN_REGEX if app_env in {"development", "dev"} else None - ) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, - allow_origin_regex=allow_origin_regex, + allow_origin_regex=CORS_HTTP_ORIGIN_REGEX, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/doc/operations-and-configuration.md b/doc/operations-and-configuration.md index cf8b9ca..3c83e29 100644 --- a/doc/operations-and-configuration.md +++ b/doc/operations-and-configuration.md @@ -116,9 +116,8 @@ Frontend runtime API target: - `VITE_API_BASE` in `docker-compose.yml` frontend service (optional override) - `VITE_API_TOKEN` in `docker-compose.yml` frontend service (optional compatibility fallback only) -When `VITE_API_BASE` is unset, frontend API helpers call relative `/api/v1` paths and the Vite dev server proxy forwards requests to `VITE_DEV_PROXY_TARGET` (defaults to `http://api:8000` in docker-compose). - -This avoids browser cross-origin/CORS failures for LAN-hosted development. +When `VITE_API_BASE` is unset, frontend API helpers resolve to: +- `http://:8000/api/v1` Frontend API authentication behavior: - `frontend/src/lib/api.ts` resolves bearer tokens at request time in this order: @@ -167,13 +166,14 @@ Retention settings are used by worker cleanup and by `POST /api/v1/processing/lo - `documents` endpoints: user token or admin token - `settings` and `processing/logs` endpoints: admin token only - Development environments can allow tokenless user-role access for document/search routes via `ALLOW_DEVELOPMENT_ANONYMOUS_USER_ACCESS=true`; production remains token-enforced. -- Development CORS allows localhost and RFC1918 private-network origins via regex in addition to explicit `CORS_ORIGINS`, so LAN-hosted frontend access remains functional. +- CORS allows HTTP and HTTPS origins by regex in addition to explicit `CORS_ORIGINS`, so LAN and public-domain frontend origins are accepted. - Authentication fails closed when `ADMIN_API_TOKEN` is not configured and admin access is requested. - Document preview endpoint blocks inline rendering for script-capable MIME types and forces attachment responses for active content. - Provider base URLs are validated on settings updates and before outbound model calls: - - allowlist enforcement (`PROVIDER_BASE_URL_ALLOWLIST`) - - scheme restrictions (`https` by default) - - local/private-network blocking and per-request DNS revalidation checks for outbound runtime calls, including OCR provider path + - optional allowlist enforcement (`PROVIDER_BASE_URL_ALLOWLIST`) + - optional scheme restrictions (`PROVIDER_BASE_URL_ALLOW_HTTP`) + - optional private-network restrictions (`PROVIDER_BASE_URL_ALLOW_PRIVATE_NETWORK`) + - per-request DNS revalidation checks for outbound runtime calls, including OCR provider path - Upload and archive safety guards are enforced: - `POST /api/v1/documents/upload` requires `Content-Length` and enforces file-count, per-file size, and total request size limits - `OPTIONS /api/v1/documents/upload` CORS preflight is excluded from `Content-Length` enforcement diff --git a/docker-compose.yml b/docker-compose.yml index b362213..caa308e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,9 +47,9 @@ services: ADMIN_API_TOKEN: ${ADMIN_API_TOKEN:?ADMIN_API_TOKEN must be set} USER_API_TOKEN: ${USER_API_TOKEN:?USER_API_TOKEN must be set} APP_SETTINGS_ENCRYPTION_KEY: ${APP_SETTINGS_ENCRYPTION_KEY:?APP_SETTINGS_ENCRYPTION_KEY must be set} - PROVIDER_BASE_URL_ALLOWLIST: '${PROVIDER_BASE_URL_ALLOWLIST:-["api.openai.com"]}' - PROVIDER_BASE_URL_ALLOW_HTTP: ${PROVIDER_BASE_URL_ALLOW_HTTP:-false} - PROVIDER_BASE_URL_ALLOW_PRIVATE_NETWORK: ${PROVIDER_BASE_URL_ALLOW_PRIVATE_NETWORK:-false} + PROVIDER_BASE_URL_ALLOWLIST: '${PROVIDER_BASE_URL_ALLOWLIST:-[]}' + PROVIDER_BASE_URL_ALLOW_HTTP: ${PROVIDER_BASE_URL_ALLOW_HTTP:-true} + PROVIDER_BASE_URL_ALLOW_PRIVATE_NETWORK: ${PROVIDER_BASE_URL_ALLOW_PRIVATE_NETWORK:-true} OCR_LANGUAGES: eng,deu PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:8000} CORS_ORIGINS: '${CORS_ORIGINS:-["http://localhost:5173","http://localhost:3000"]}' @@ -90,9 +90,9 @@ services: ADMIN_API_TOKEN: ${ADMIN_API_TOKEN:?ADMIN_API_TOKEN must be set} USER_API_TOKEN: ${USER_API_TOKEN:?USER_API_TOKEN must be set} APP_SETTINGS_ENCRYPTION_KEY: ${APP_SETTINGS_ENCRYPTION_KEY:?APP_SETTINGS_ENCRYPTION_KEY must be set} - PROVIDER_BASE_URL_ALLOWLIST: '${PROVIDER_BASE_URL_ALLOWLIST:-["api.openai.com"]}' - PROVIDER_BASE_URL_ALLOW_HTTP: ${PROVIDER_BASE_URL_ALLOW_HTTP:-false} - PROVIDER_BASE_URL_ALLOW_PRIVATE_NETWORK: ${PROVIDER_BASE_URL_ALLOW_PRIVATE_NETWORK:-false} + PROVIDER_BASE_URL_ALLOWLIST: '${PROVIDER_BASE_URL_ALLOWLIST:-[]}' + PROVIDER_BASE_URL_ALLOW_HTTP: ${PROVIDER_BASE_URL_ALLOW_HTTP:-true} + PROVIDER_BASE_URL_ALLOW_PRIVATE_NETWORK: ${PROVIDER_BASE_URL_ALLOW_PRIVATE_NETWORK:-true} OCR_LANGUAGES: eng,deu PUBLIC_BASE_URL: http://localhost:8000 TYPESENSE_PROTOCOL: http @@ -120,7 +120,6 @@ services: context: ./frontend environment: VITE_API_BASE: ${VITE_API_BASE:-} - VITE_DEV_PROXY_TARGET: ${VITE_DEV_PROXY_TARGET:-http://api:8000} VITE_API_TOKEN: ${VITE_API_TOKEN:-} ports: - "${HOST_BIND_IP:-127.0.0.1}:5173:5173" diff --git a/frontend/src/lib/api.test.ts b/frontend/src/lib/api.test.ts index 1df065e..6d5f551 100644 --- a/frontend/src/lib/api.test.ts +++ b/frontend/src/lib/api.test.ts @@ -97,11 +97,11 @@ async function runApiTests(): Promise { assert(await thumbnail.text() === 'preview-bytes', 'Thumbnail blob bytes mismatch'); assert(await preview.text() === 'preview-bytes', 'Preview blob bytes mismatch'); assert( - requestUrls[0] === '/api/v1/documents/doc-1/thumbnail', + requestUrls[0] === 'http://localhost:8000/api/v1/documents/doc-1/thumbnail', `Unexpected thumbnail URL ${requestUrls[0]}`, ); assert( - requestUrls[1] === '/api/v1/documents/doc-1/preview', + requestUrls[1] === 'http://localhost:8000/api/v1/documents/doc-1/preview', `Unexpected preview URL ${requestUrls[1]}`, ); assert(requestAuthHeaders[0] === null, `Expected no auth header for thumbnail request, got "${requestAuthHeaders[0]}"`); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 5d805b0..395f6d2 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -14,7 +14,7 @@ import type { } from '../types'; /** - * Resolves backend base URL from environment with same-origin proxy fallback. + * Resolves backend base URL from environment with host-derived HTTP fallback. */ function resolveApiBase(): string { const envValue = import.meta.env?.VITE_API_BASE; @@ -25,7 +25,10 @@ function resolveApiBase(): string { } } - return '/api/v1'; + if (typeof window !== 'undefined' && window.location?.hostname) { + return `${window.location.protocol}//${window.location.hostname}:8000/api/v1`; + } + return 'http://localhost:8000/api/v1'; } const API_BASE = resolveApiBase(); @@ -54,6 +57,8 @@ let runtimeTokenResolver: ApiTokenResolver | null = null; type ApiRequestInit = Omit & { headers?: HeadersInit }; +type ApiErrorPayload = { detail?: string } | null; + /** * Normalizes candidate token values by trimming whitespace and filtering non-string values. */ @@ -163,6 +168,21 @@ function apiRequest(input: string, init: ApiRequestInit = {}): Promise }); } +/** + * Extracts backend error detail text from JSON payloads when available. + */ +async function responseErrorDetail(response: Response): Promise { + try { + const payload = (await response.json()) as ApiErrorPayload; + if (payload && typeof payload.detail === 'string' && payload.detail.trim()) { + return payload.detail.trim(); + } + } catch { + return ''; + } + return ''; +} + /** * Encodes query parameters while skipping undefined and null values. */ @@ -607,7 +627,8 @@ export async function updateAppSettings(payload: AppSettingsUpdate): Promise; } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 820a5b5..5c6934f 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -10,11 +10,5 @@ export default defineConfig({ server: { host: '0.0.0.0', port: 5173, - proxy: { - '/api/v1': { - target: process.env.VITE_DEV_PROXY_TARGET ?? 'http://localhost:8000', - changeOrigin: true, - }, - }, }, });