diff --git a/.env.example b/.env.example index f04f23e..b8421a0 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,12 @@ NODE_ENV=development # GitHub OAuth GITHUB_CLIENT_ID=your_client_id_here GITHUB_CLIENT_SECRET=your_secret_here -UI_BASE_URL=http://localhost:8001 +# UI origin(s). Comma-separated list โ€” ALL are added to the CORS allowlist. +# The first entry is the "primary" origin used for OAuth redirects. +# Examples: +# UI_BASE_URL=http://localhost:5173 +# UI_BASE_URL=https://thehumanpatternlab.com,https://ironkitsune.tech +UI_BASE_URL=http://localhost:5173 # DB DB_PATH=/path/to/lab.db diff --git a/src/app.ts b/src/app.ts index 37c29f6..f3ab14e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -58,9 +58,20 @@ export function createApp() { const isTest = env.NODE_ENV === "test"; const isProd = env.NODE_ENV === "production"; - // This must match the browser's Origin exactly (no trailing slash). - // Example: "https://thehumanpatternlab.com" - const uiOrigin = env.UI_BASE_URL ?? "http://localhost:5173"; + // CORS origins come from UI_BASE_URL (comma-separated allowed). + // Browser Origin header must match EXACTLY (no trailing slash). + // Example: UI_BASE_URL="https://thehumanpatternlab.com,https://ironkitsune.tech" + const allowedOrigins = + env.UI_ALLOWED_ORIGINS.length > 0 + ? env.UI_ALLOWED_ORIGINS + : ["http://localhost:5173"]; + const corsOrigin = (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => { + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error(`CORS: origin ${origin} not allowed`)); + } + }; /* =========================================================== 5) CORS (BEFORE SESSION IF CROSS-ORIGIN) @@ -78,13 +89,13 @@ export function createApp() { =========================================================== */ app.use( cors({ - origin: uiOrigin, + origin: corsOrigin, credentials: true, }) ); // โœ… Force-handle ALL preflight requests - app.options(/.*/, cors({ origin: uiOrigin, credentials: true })); + app.options(/.*/, cors({ origin: corsOrigin, credentials: true })); app.use((req, _res, next) => { if (req.method === "OPTIONS") { console.log("๐Ÿงช Preflight:", req.headers.origin, req.headers["access-control-request-method"], req.url); @@ -240,3 +251,4 @@ export function createApp() { registerOpenApiRoutes(app); return app; } + diff --git a/src/env.ts b/src/env.ts index 8582d7d..fcc9986 100644 --- a/src/env.ts +++ b/src/env.ts @@ -16,6 +16,7 @@ type Env = { SESSION_SECRET?: string; UI_BASE_URL?: string; + UI_ALLOWED_ORIGINS: string[]; // optional OAuth vars (used by auth.ts via process.env) OAUTH_GITHUB_CLIENT_ID?: string; @@ -64,6 +65,14 @@ function normalizeNodeEnv(value: string): NodeEnv { throw new EnvError('Invalid NODE_ENV="' + value + '". Use "development", "test", or "production".'); } +function parseOriginList(value: string | undefined): string[] { + if (!value) return []; + return value + .split(",") + .map((s) => s.trim().replace(/\/$/, "")) // drop trailing slash + .filter((s) => s.length > 0); +} + function parsePort(value: string | undefined, fallback: number): number { if (value == null || value.trim() === "") return fallback; const n = Number(value); @@ -97,7 +106,8 @@ function validateEnv(input: NodeJS.ProcessEnv): Env { DB_PATH, SESSION_SECRET, - UI_BASE_URL: input.UI_BASE_URL?.trim(), + UI_BASE_URL: parseOriginList(input.UI_BASE_URL)[0], + UI_ALLOWED_ORIGINS: parseOriginList(input.UI_BASE_URL), OAUTH_GITHUB_CLIENT_ID: input.OAUTH_GITHUB_CLIENT_ID?.trim(), OAUTH_GITHUB_CLIENT_SECRET: input.OAUTH_GITHUB_CLIENT_SECRET?.trim(), diff --git a/src/routes/adminRoutes.ts b/src/routes/adminRoutes.ts index 2a3b041..0c688cd 100644 --- a/src/routes/adminRoutes.ts +++ b/src/routes/adminRoutes.ts @@ -7,6 +7,7 @@ import passport, { requireAdmin, isGithubOAuthEnabled } from "../auth.js"; import { requireAuth } from "../middleware/requireAuth.js"; import { syncLabNotesFromFs, SyncCounts } from "../services/syncLabNotesFromFs.js"; import { normalizeLocale, sha256Hex } from "../lib/helpers.js"; +import { env } from "../env.js"; marked.setOptions({ gfm: true, @@ -14,8 +15,9 @@ marked.setOptions({ }); export function registerAdminRoutes(app: any, db: Database.Database) { - // Must match your UI origin exactly (no trailing slash) - const UI_BASE_URL = process.env.UI_BASE_URL ?? "http://localhost:8001"; + // Primary UI origin (first entry of UI_BASE_URL). Used for OAuth redirects. + // For CORS allowlist behavior, see env.UI_ALLOWED_ORIGINS. + const UI_BASE_URL = env.UI_BASE_URL ?? "http://localhost:5173"; // --------------------------------------------------------------------------- // Admin: list Lab Notes (protected)