Skip to content

Commit 03dbae3

Browse files
authored
Merge pull request #27 from AdaInTheLab/feat/multi-origin-cors
⚙️ feat: Support multiple CORS origins via comma-separated UI_BASE_URL
2 parents 38f9139 + c0d1b96 commit 03dbae3

File tree

4 files changed

+38
-9
lines changed

4 files changed

+38
-9
lines changed

.env.example

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ NODE_ENV=development
55
# GitHub OAuth
66
GITHUB_CLIENT_ID=your_client_id_here
77
GITHUB_CLIENT_SECRET=your_secret_here
8-
UI_BASE_URL=http://localhost:8001
8+
# UI origin(s). Comma-separated list — ALL are added to the CORS allowlist.
9+
# The first entry is the "primary" origin used for OAuth redirects.
10+
# Examples:
11+
# UI_BASE_URL=http://localhost:5173
12+
# UI_BASE_URL=https://thehumanpatternlab.com,https://ironkitsune.tech
13+
UI_BASE_URL=http://localhost:5173
914

1015
# DB
1116
DB_PATH=/path/to/lab.db

src/app.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,20 @@ export function createApp() {
5858
const isTest = env.NODE_ENV === "test";
5959
const isProd = env.NODE_ENV === "production";
6060

61-
// This must match the browser's Origin exactly (no trailing slash).
62-
// Example: "https://thehumanpatternlab.com"
63-
const uiOrigin = env.UI_BASE_URL ?? "http://localhost:5173";
61+
// CORS origins come from UI_BASE_URL (comma-separated allowed).
62+
// Browser Origin header must match EXACTLY (no trailing slash).
63+
// Example: UI_BASE_URL="https://thehumanpatternlab.com,https://ironkitsune.tech"
64+
const allowedOrigins =
65+
env.UI_ALLOWED_ORIGINS.length > 0
66+
? env.UI_ALLOWED_ORIGINS
67+
: ["http://localhost:5173"];
68+
const corsOrigin = (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
69+
if (!origin || allowedOrigins.includes(origin)) {
70+
callback(null, true);
71+
} else {
72+
callback(new Error(`CORS: origin ${origin} not allowed`));
73+
}
74+
};
6475

6576
/* ===========================================================
6677
5) CORS (BEFORE SESSION IF CROSS-ORIGIN)
@@ -78,13 +89,13 @@ export function createApp() {
7889
=========================================================== */
7990
app.use(
8091
cors({
81-
origin: uiOrigin,
92+
origin: corsOrigin,
8293
credentials: true,
8394
})
8495
);
8596

8697
// ✅ Force-handle ALL preflight requests
87-
app.options(/.*/, cors({ origin: uiOrigin, credentials: true }));
98+
app.options(/.*/, cors({ origin: corsOrigin, credentials: true }));
8899
app.use((req, _res, next) => {
89100
if (req.method === "OPTIONS") {
90101
console.log("🧪 Preflight:", req.headers.origin, req.headers["access-control-request-method"], req.url);
@@ -240,3 +251,4 @@ export function createApp() {
240251
registerOpenApiRoutes(app);
241252
return app;
242253
}
254+

src/env.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Env = {
1616
SESSION_SECRET?: string;
1717

1818
UI_BASE_URL?: string;
19+
UI_ALLOWED_ORIGINS: string[];
1920

2021
// optional OAuth vars (used by auth.ts via process.env)
2122
OAUTH_GITHUB_CLIENT_ID?: string;
@@ -64,6 +65,14 @@ function normalizeNodeEnv(value: string): NodeEnv {
6465
throw new EnvError('Invalid NODE_ENV="' + value + '". Use "development", "test", or "production".');
6566
}
6667

68+
function parseOriginList(value: string | undefined): string[] {
69+
if (!value) return [];
70+
return value
71+
.split(",")
72+
.map((s) => s.trim().replace(/\/$/, "")) // drop trailing slash
73+
.filter((s) => s.length > 0);
74+
}
75+
6776
function parsePort(value: string | undefined, fallback: number): number {
6877
if (value == null || value.trim() === "") return fallback;
6978
const n = Number(value);
@@ -97,7 +106,8 @@ function validateEnv(input: NodeJS.ProcessEnv): Env {
97106
DB_PATH,
98107
SESSION_SECRET,
99108

100-
UI_BASE_URL: input.UI_BASE_URL?.trim(),
109+
UI_BASE_URL: parseOriginList(input.UI_BASE_URL)[0],
110+
UI_ALLOWED_ORIGINS: parseOriginList(input.UI_BASE_URL),
101111

102112
OAUTH_GITHUB_CLIENT_ID: input.OAUTH_GITHUB_CLIENT_ID?.trim(),
103113
OAUTH_GITHUB_CLIENT_SECRET: input.OAUTH_GITHUB_CLIENT_SECRET?.trim(),

src/routes/adminRoutes.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ import passport, { requireAdmin, isGithubOAuthEnabled } from "../auth.js";
77
import { requireAuth } from "../middleware/requireAuth.js";
88
import { syncLabNotesFromFs, SyncCounts } from "../services/syncLabNotesFromFs.js";
99
import { normalizeLocale, sha256Hex } from "../lib/helpers.js";
10+
import { env } from "../env.js";
1011

1112
marked.setOptions({
1213
gfm: true,
1314
breaks: false, // ✅ strict
1415
});
1516

1617
export function registerAdminRoutes(app: any, db: Database.Database) {
17-
// Must match your UI origin exactly (no trailing slash)
18-
const UI_BASE_URL = process.env.UI_BASE_URL ?? "http://localhost:8001";
18+
// Primary UI origin (first entry of UI_BASE_URL). Used for OAuth redirects.
19+
// For CORS allowlist behavior, see env.UI_ALLOWED_ORIGINS.
20+
const UI_BASE_URL = env.UI_BASE_URL ?? "http://localhost:5173";
1921

2022
// ---------------------------------------------------------------------------
2123
// Admin: list Lab Notes (protected)

0 commit comments

Comments
 (0)