| /** | |
| * API Authentication Guard β Shared utility for protecting management API routes. | |
| * | |
| * Provides dual-mode auth: JWT cookie (dashboard session) or Bearer API key. | |
| * Used by the middleware (proxy.ts) to guard /api/* management routes. | |
| * | |
| * @module shared/utils/apiAuth | |
| */ | |
| import { jwtVerify } from "jose"; | |
| import { getSettings } from "@/lib/localDb"; | |
| // ββββββββββββββββ Public Routes (No Auth Required) ββββββββββββββββ | |
| /** | |
| * Routes that are ALWAYS accessible without authentication. | |
| * Pattern matching: startsWith check against the pathname. | |
| */ | |
| const PUBLIC_API_ROUTES = [ | |
| // Auth flow β must be accessible to unauthenticated users | |
| "/api/auth/login", | |
| "/api/auth/logout", | |
| "/api/auth/status", | |
| // Settings check β used by login page / onboarding | |
| "/api/settings/require-login", | |
| // Init β first-run setup | |
| "/api/init", | |
| // Health monitoring β probes must work without auth | |
| "/api/monitoring/health", | |
| // LLM proxy routes β use their own API key auth in the SSE layer | |
| "/api/v1/", | |
| // Cloud routes β use Bearer API key auth internally | |
| "/api/cloud/", | |
| // OAuth callback routes β provider redirects back here | |
| "/api/oauth/", | |
| ]; | |
| // ββββββββββββββββ Auth Verification ββββββββββββββββ | |
| /** | |
| * Check if a request is authenticated via JWT cookie or Bearer API key. | |
| * | |
| * @returns null if authenticated, error message string if not | |
| */ | |
| export async function verifyAuth(request: any): Promise<string | null> { | |
| // 1. Check JWT cookie (dashboard session) | |
| const token = request.cookies.get("auth_token")?.value; | |
| if (token && process.env.JWT_SECRET) { | |
| try { | |
| const secret = new TextEncoder().encode(process.env.JWT_SECRET); | |
| await jwtVerify(token, secret); | |
| return null; // β Authenticated via cookie | |
| } catch { | |
| // Invalid/expired token β fall through to API key check | |
| } | |
| } | |
| // 2. Check Bearer API key | |
| const authHeader = request.headers.get("authorization"); | |
| if (authHeader?.startsWith("Bearer ")) { | |
| const apiKey = authHeader.slice(7); | |
| try { | |
| // Dynamic import to avoid circular dependencies during build | |
| const { validateApiKey } = await import("@/lib/db/apiKeys"); | |
| const isValid = await validateApiKey(apiKey); | |
| if (isValid) return null; // β Authenticated via API key | |
| } catch { | |
| // DB not ready or import error β deny access | |
| } | |
| } | |
| return "Authentication required"; | |
| } | |
| /** | |
| * Check if a route is in the public (no-auth) allowlist. | |
| */ | |
| export function isPublicRoute(pathname: string): boolean { | |
| return PUBLIC_API_ROUTES.some((route) => pathname.startsWith(route)); | |
| } | |
| /** | |
| * Check if authentication is required based on settings. | |
| * If requireLogin is false AND no password is set, auth is skipped. | |
| */ | |
| export async function isAuthRequired(): Promise<boolean> { | |
| try { | |
| const settings = await getSettings(); | |
| if (settings.requireLogin === false) return false; | |
| // If no password set and no env override, don't require auth (fresh install) | |
| if (!settings.password && !process.env.INITIAL_PASSWORD) return false; | |
| return true; | |
| } catch { | |
| // On error, require auth (secure by default) | |
| return true; | |
| } | |
| } | |