feat(admin): add-node form + After Effects version dropdown

- Nodes page: "+ افزودن نود" opens a full-screen form (name, region, IP, worker
  port, AE version, node kind, RAM, CPU, priority) → POST /v1/nodes
- current_ae_version is now a dropdown (2025…2020, matching the ae_version DB
  enum) instead of free text; node_kind is a dropdown (Shared/Dedicated/Spot)
- new POST /api/admin/nodes proxy route (forwards body; admin-gated). The backend
  POST /v1/nodes existed but had no UI — you couldn't define nodes before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-03 01:00:14 +03:30
parent 5b6f3e851b
commit ffc0c5d6d5
2 changed files with 112 additions and 3 deletions
+32
View File
@@ -0,0 +1,32 @@
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 const runtime = "nodejs";
/** Create a render node (POST body forwarded to the gateway). Admin-gated. */
export async function POST(req: NextRequest) {
const token = await getAccessToken();
if (!token) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
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 body = await req.text();
const res = await fetch(gatewayUrl("/v1/nodes"), {
method: "POST",
cache: "no-store",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body,
});
const data = await res.json().catch(() => null);
if (!res.ok) {
return NextResponse.json(
{ error: data?.message ?? data?.error?.message ?? "Gateway error" },
{ status: res.status }
);
}
return NextResponse.json(data ?? { ok: true });
}