| import { NextResponse } from "next/server"; |
| import { jwtVerify, SignJWT } from "jose"; |
| import { generateRequestId } from "./shared/utils/requestId"; |
| import { getSettings } from "./lib/localDb"; |
| import { isPublicRoute, verifyAuth, isAuthRequired } from "./shared/utils/apiAuth"; |
| import { checkBodySize, getBodySizeLimit } from "./shared/middleware/bodySizeGuard"; |
| import { isDraining } from "./lib/gracefulShutdown"; |
| import { isModelSyncInternalRequest } from "./shared/services/modelSyncScheduler"; |
|
|
| const SECRET = new TextEncoder().encode(process.env.JWT_SECRET || ""); |
|
|
| export async function proxy(request: any) { |
| const { pathname } = request.nextUrl; |
|
|
| |
| const requestId = generateRequestId(); |
| const response = NextResponse.next(); |
| response.headers.set("X-Request-Id", requestId); |
|
|
| |
| if (isDraining() && pathname.startsWith("/api/")) { |
| return NextResponse.json( |
| { |
| error: { |
| code: "SERVICE_UNAVAILABLE", |
| message: "Server is shutting down", |
| correlation_id: requestId, |
| }, |
| }, |
| { status: 503 } |
| ); |
| } |
|
|
| |
| if (pathname.startsWith("/api/") && request.method !== "GET" && request.method !== "OPTIONS") { |
| const bodySizeRejection = checkBodySize(request, getBodySizeLimit(pathname)); |
| if (bodySizeRejection) return bodySizeRejection; |
| } |
|
|
| |
| if (pathname.startsWith("/api/") && !pathname.startsWith("/api/v1/")) { |
| |
| if (isPublicRoute(pathname)) { |
| return response; |
| } |
|
|
| |
| if ( |
| isModelSyncInternalRequest(request) && |
| /^\/api\/providers\/[^/]+\/(sync-models|models)$/.test(pathname) |
| ) { |
| return response; |
| } |
|
|
| |
| const authRequired = await isAuthRequired(); |
| if (!authRequired) { |
| return response; |
| } |
|
|
| |
| const authError = await verifyAuth(request); |
| if (authError) { |
| return NextResponse.json( |
| { |
| error: { |
| code: "AUTH_001", |
| message: authError, |
| correlation_id: requestId, |
| }, |
| }, |
| { status: 401 } |
| ); |
| } |
| } |
|
|
| |
| if (pathname.startsWith("/dashboard")) { |
| |
| if (pathname.startsWith("/dashboard/onboarding")) { |
| return response; |
| } |
|
|
| try { |
| |
| const settings = await getSettings(); |
| |
| if (settings.requireLogin === false) { |
| return response; |
| } |
| |
| |
| if (!settings.setupComplete && !settings.password && !process.env.INITIAL_PASSWORD) { |
| return response; |
| } |
| } catch (err) { |
| |
| console.error("[Middleware] settings_error: Settings read failed:", err.message, { |
| path: pathname, |
| requestId, |
| }); |
| |
| } |
|
|
| const token = request.cookies.get("auth_token")?.value; |
|
|
| if (token) { |
| try { |
| const { payload } = await jwtVerify(token, SECRET); |
|
|
| |
| const exp = payload.exp as number; |
| const now = Math.floor(Date.now() / 1000); |
| const REFRESH_WINDOW = 7 * 24 * 60 * 60; |
| if (exp && exp - now < REFRESH_WINDOW) { |
| try { |
| const freshToken = await new SignJWT({ authenticated: true }) |
| .setProtectedHeader({ alg: "HS256" }) |
| .setExpirationTime("30d") |
| .sign(SECRET); |
|
|
| |
| const fwdProto = (request.headers.get("x-forwarded-proto") || "") |
| .split(",")[0] |
| .trim() |
| .toLowerCase(); |
| const isHttps = fwdProto === "https" || request.nextUrl?.protocol === "https:"; |
| const useSecure = process.env.AUTH_COOKIE_SECURE === "true" || isHttps; |
|
|
| response.cookies.set("auth_token", freshToken, { |
| httpOnly: true, |
| secure: useSecure, |
| sameSite: "lax", |
| path: "/", |
| }); |
| console.log( |
| `[Middleware] JWT auto-refreshed for ${pathname} (was expiring in ${Math.round((exp - now) / 3600)}h)` |
| ); |
| } catch (refreshErr) { |
| |
| console.error("[Middleware] JWT auto-refresh failed:", refreshErr.message); |
| } |
| } |
|
|
| return response; |
| } catch (err) { |
| |
| console.error("[Middleware] auth_error: JWT verification failed:", err.message, { |
| path: pathname, |
| tokenPresent: true, |
| requestId, |
| }); |
| const redirectResponse = NextResponse.redirect(new URL("/login", request.url)); |
| redirectResponse.cookies.delete("auth_token"); |
| return redirectResponse; |
| } |
| } |
|
|
| return NextResponse.redirect(new URL("/login", request.url)); |
| } |
|
|
| |
| if (pathname === "/") { |
| return NextResponse.redirect(new URL("/dashboard", request.url)); |
| } |
|
|
| return response; |
| } |
|
|
| export const config = { |
| matcher: ["/", "/dashboard/:path*", "/api/:path*"], |
| }; |
|
|