12773e125a
Token auto-refresh (middleware): - Proactively refresh fr_access when < 120s remain — no more silent 15-min kick - Inlines /v1/auth/refresh call in middleware, stamps new cookies on response - /admin/* protected: is_admin JWT claim required, else redirect /dashboard - apiFetch() (src/lib/api/fetch.ts): client-side 401 → auto-refresh → retry; de-duplicates concurrent refresh calls; redirects to /auth on failure Studio → Render V2 wiring: - scenes[] no longer sent to POST /api/render (V2 render-svc fetches project from Studio service via saved_project_id directly) - renderRequestSchema.scenes is now optional - RenderModal uses apiFetch for auto-refresh on 401 during polling Admin panel (/admin/*): - Admin layout: server-side is_admin guard + top nav (Nodes, Render Queue) - /admin/nodes: lists all nodes from GET /v1/nodes with status badges, heartbeat age, slot usage, tags; Drain (PATCH status=Draining) + Release actions - /admin/renders: render job table with step filter tabs; progress bars, error messages, Retry + Cancel per-row actions; polls GET /v1/renders - API proxy routes: /api/admin/nodes/:id/drain|release, /api/admin/renders/:id/retry|cancel — all validate is_admin in JWT before proxying Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
52 lines
1.4 KiB
TypeScript
52 lines
1.4 KiB
TypeScript
/**
|
|
* Shared helper for admin action proxy routes.
|
|
* Validates the caller is an admin (checks is_admin in the JWT), then
|
|
* proxies the action to the V2 gateway.
|
|
*/
|
|
import { type NextRequest, NextResponse } from "next/server";
|
|
import { gatewayUrl } from "@/lib/api/gateway";
|
|
import { getAccessToken } from "@/lib/auth/session";
|
|
import { decodeJwt } from "@/lib/auth/jwt";
|
|
|
|
export async function adminProxy(
|
|
_req: NextRequest,
|
|
gatewayPath: string,
|
|
method: string = "POST"
|
|
): Promise<NextResponse> {
|
|
const token = await getAccessToken();
|
|
if (!token) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
// Quick admin check on the server side before forwarding
|
|
const claims = decodeJwt(token);
|
|
const isAdmin =
|
|
String(claims?.is_admin) === "true" ||
|
|
claims?.is_admin === true ||
|
|
String(claims?.is_tenant_admin) === "true";
|
|
|
|
if (!isAdmin) {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
|
|
const res = await fetch(gatewayUrl(gatewayPath), {
|
|
method,
|
|
cache: "no-store",
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => null) as { message?: string } | null;
|
|
return NextResponse.json(
|
|
{ error: err?.message ?? "Gateway error" },
|
|
{ status: res.status }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json({ ok: true });
|
|
}
|