From aaebd14c6034738879b27223e7beb019bd18101a Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Wed, 13 May 2026 19:01:30 +0800 Subject: [PATCH] feat(oidc): register cookie and OIDC plugins, extend config endpoint --- apps/api/src/index.ts | 41 +++++++++++++++++++++++++++++--- tests/integration/test-server.ts | 20 +++++++++++++--- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 02da9c17..dbebaeb1 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import cookie from "@fastify/cookie"; import cors from "@fastify/cors"; import rateLimit from "@fastify/rate-limit"; import { getDispatcherStatus, initDispatcher, isGpuAvailable } from "@snapotter/ai"; @@ -15,6 +16,7 @@ import { ensureAiDirs, recoverInterruptedInstalls } from "./lib/feature-status.j import { shutdownWorkerPool } from "./lib/worker-pool.js"; import { requirePermission } from "./permissions.js"; import { authMiddleware, authRoutes, ensureDefaultAdmin } from "./plugins/auth.js"; +import { oidcRoutes } from "./plugins/oidc.js"; import { registerStatic } from "./plugins/static.js"; import { registerUpload } from "./plugins/upload.js"; import { analyticsRoutes } from "./routes/analytics.js"; @@ -70,6 +72,22 @@ function ensureDefaultSettings() { } ensureDefaultSettings(); + +if (!env.COOKIE_SECRET) { + const existing = db + .select() + .from(schema.settings) + .where(eq(schema.settings.key, "cookie_secret")) + .get(); + if (existing) { + (env as Record).COOKIE_SECRET = existing.value; + } else { + const generated = randomUUID() + randomUUID(); + db.insert(schema.settings).values({ key: "cookie_secret", value: generated }).run(); + (env as Record).COOKIE_SECRET = generated; + } +} + await initAnalytics(); // Mark any jobs left in processing/queued from a previous unclean shutdown @@ -151,12 +169,21 @@ await app.register(rateLimit, { // Multipart upload support await registerUpload(app); +// Cookie support (required for OIDC state and session cookies) +await app.register(cookie, { + secret: env.COOKIE_SECRET, + hook: "onRequest", +}); + // Auth middleware (must be registered before routes it protects) await authMiddleware(app); // Auth routes await authRoutes(app); +// OIDC routes +await oidcRoutes(app); + // File upload/download routes await fileRoutes(app); @@ -247,9 +274,17 @@ app.get("/api/v1/admin/health", async (request, reply) => { }); // Public config endpoint (for frontend to know if auth is required) -app.get("/api/v1/config/auth", async () => ({ - authEnabled: env.AUTH_ENABLED, -})); +app.get("/api/v1/config/auth", async () => { + const config: Record = { + authEnabled: env.AUTH_ENABLED, + }; + if (env.OIDC_ENABLED) { + config.oidcEnabled = true; + config.oidcProviderName = env.OIDC_PROVIDER_NAME || null; + config.oidcLoginUrl = "/api/auth/oidc/login"; + } + return config; +}); // Serve SPA in production if (process.env.NODE_ENV === "production") { diff --git a/tests/integration/test-server.ts b/tests/integration/test-server.ts index 5e2d0464..a002eb6a 100644 --- a/tests/integration/test-server.ts +++ b/tests/integration/test-server.ts @@ -19,6 +19,7 @@ import { dirname } from "node:path"; mkdirSync(dirname(process.env.DB_PATH!), { recursive: true }); mkdirSync(process.env.WORKSPACE_PATH!, { recursive: true }); +import cookie from "@fastify/cookie"; import cors from "@fastify/cors"; import { APP_VERSION } from "@snapotter/shared"; import { eq } from "drizzle-orm"; @@ -31,6 +32,7 @@ import { db, schema } from "../../apps/api/src/db/index.js"; import { runMigrations } from "../../apps/api/src/db/migrate.js"; import { requirePermission } from "../../apps/api/src/permissions.js"; import { authMiddleware, authRoutes, ensureDefaultAdmin } from "../../apps/api/src/plugins/auth.js"; +import { oidcRoutes } from "../../apps/api/src/plugins/oidc.js"; import { registerUpload } from "../../apps/api/src/plugins/upload.js"; import { analyticsRoutes } from "../../apps/api/src/routes/analytics.js"; import { apiKeyRoutes } from "../../apps/api/src/routes/api-keys.js"; @@ -80,12 +82,18 @@ export async function buildTestApp(): Promise { // Multipart upload support await registerUpload(app); + // Cookie support + await app.register(cookie, { secret: "test-cookie-secret", hook: "onRequest" }); + // Auth middleware (must be registered before routes) await authMiddleware(app); // Auth routes await authRoutes(app); + // OIDC routes + await oidcRoutes(app); + // File upload/download routes await fileRoutes(app); @@ -161,9 +169,15 @@ export async function buildTestApp(): Promise { }); // Public config endpoint - app.get("/api/v1/config/auth", async () => ({ - authEnabled: env.AUTH_ENABLED, - })); + app.get("/api/v1/config/auth", async () => { + const config: Record = { authEnabled: env.AUTH_ENABLED }; + if (env.OIDC_ENABLED) { + config.oidcEnabled = true; + config.oidcProviderName = env.OIDC_PROVIDER_NAME || null; + config.oidcLoginUrl = "/api/auth/oidc/login"; + } + return config; + }); // Ensure Fastify is ready (all plugins loaded) await app.ready();