mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
* chore(release): v3.2.8 — Docker auto-update UI and cache analytics fixes * fix(sse): remove race condition in cache metrics tracking (#758) - Remove in-memory metrics tracking (currentMetrics, trackCacheMetrics, updateCacheMetrics) - Cache metrics now computed on-the-fly from usage_history table (single source of truth) - Fixes CRITICAL issue from code review: concurrent requests overwriting metrics - Fixes WARNING: duplicate metric tracking logic in streaming/non-streaming paths Ref: PR #752 (merged before this fix was included) * fix: handle allRateLimited credentials & forward extra body keys in embeddings/images routes (#757) * fix: handle allRateLimited credentials in embeddings and images routes When getProviderCredentials() returns an allRateLimited object (truthy, but without apiKey/accessToken), the embeddings and images routes incorrectly passed it to handlers as valid credentials. The handlers then sent upstream requests without Authorization headers, causing 401 errors from providers (e.g. NVIDIA NIM). This only manifested under concurrent requests: a chat/completions call could trigger rate limiting on a provider account, and a simultaneous embeddings request would receive the allRateLimited sentinel — but treat it as valid credentials. The chat pipeline already handled this case correctly. This commit adds the same allRateLimited guard to all affected routes: - POST /v1/embeddings - POST /v1/providers/{provider}/embeddings - POST /v1/images/generations - POST /v1/providers/{provider}/images/generations Also adds a defense-in-depth guard in the embeddings handler itself: if no auth token is available for a non-local provider, return 401 immediately instead of sending an unauthenticated request upstream. Made-with: Cursor * fix(embeddings): forward extra body keys to upstream providers The embeddings handler only forwarded model, input, dimensions, and encoding_format to upstream providers, silently dropping any additional fields. This broke asymmetric embedding APIs (e.g. NVIDIA NIM nv-embedqa-e5-v5) that require input_type, and other providers expecting user or truncate parameters. Add a KNOWN_FIELDS exclusion set and forward all unrecognized body keys to the upstream request, matching the passthrough pattern used by the chat pipeline's DefaultExecutor.transformRequest(). Made-with: Cursor * fix(auth): redirect and unconditional 401 on disabled requireLogin + fix test cases * fix(build): remove legacy proxy.ts causing Next.js build collision * fix(build): revert middleware.ts rename to proxy.ts because of Next.js Edge constraints --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: tombii <tombii@users.noreply.github.com> Co-authored-by: Gorchakov-Pressure <117600961+Gorchakov-Pressure@users.noreply.github.com>
157 lines
5.3 KiB
TypeScript
157 lines
5.3 KiB
TypeScript
/**
|
|
* 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 { cookies } from "next/headers";
|
|
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 request is authenticated — boolean convenience wrapper for route handlers.
|
|
*
|
|
* Uses `cookies()` from next/headers (App Router compatible) and Bearer API key.
|
|
* Returns true if authenticated, false otherwise.
|
|
*
|
|
* Unlike `verifyAuth`, this does NOT check `isAuthRequired()` — callers that
|
|
* need to conditionally skip auth should check that separately.
|
|
*/
|
|
export async function isAuthenticated(request: Request): Promise<boolean> {
|
|
// If settings say login/auth is disabled, treat all requests as authenticated
|
|
if (!(await isAuthRequired())) {
|
|
return true;
|
|
}
|
|
// 1. Check API key (for external clients)
|
|
const authHeader = request.headers.get("authorization");
|
|
if (authHeader?.startsWith("Bearer ")) {
|
|
const apiKey = authHeader.slice(7);
|
|
try {
|
|
const { validateApiKey } = await import("@/lib/db/apiKeys");
|
|
if (await validateApiKey(apiKey)) return true;
|
|
} catch {
|
|
// DB not ready or import error
|
|
}
|
|
}
|
|
|
|
// 2. Check JWT cookie (for dashboard session)
|
|
if (process.env.JWT_SECRET) {
|
|
try {
|
|
const cookieStore = await cookies();
|
|
const token = cookieStore.get("auth_token")?.value;
|
|
if (token) {
|
|
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
|
|
await jwtVerify(token, secret);
|
|
return true;
|
|
}
|
|
} catch {
|
|
// Invalid/expired token or cookies not available
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
// Allow access with no password set — there's nothing to authenticate against.
|
|
// This covers two cases:
|
|
// 1. Fresh installs (setupComplete=false) — first-run, no password yet
|
|
// 2. setupComplete=true but password was skipped during onboarding (#256)
|
|
// The user needs unauthenticated access to /dashboard/settings to set a password.
|
|
// Note: this is safe because Bearer API key auth is still checked in verifyAuth().
|
|
// The security concern from #151 (password row lost after being set) is handled by the
|
|
// hasPassword flag — if a password WAS set and then somehow lost, the user can use the
|
|
// reset-password CLI tool (bin/reset-password.mjs).
|
|
if (!settings.password && !process.env.INITIAL_PASSWORD) return false;
|
|
return true;
|
|
} catch {
|
|
// On error, require auth (secure by default)
|
|
return true;
|
|
}
|
|
}
|