Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 17 additions & 5 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
Expand Down Expand Up @@ -240,3 +251,4 @@ export function createApp() {
registerOpenApiRoutes(app);
return app;
}

12 changes: 11 additions & 1 deletion src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(),
Expand Down
6 changes: 4 additions & 2 deletions src/routes/adminRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ 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,
breaks: false, // ✅ strict
});

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)
Expand Down
Loading