Files
sam-portal/src/app/api/deployer/[...slug]/route.ts
T
Nurfog 8f30b1c1ec feat: auditoría de seguridad en APIs, deuda Next 16 y pnpm como gestor
Cierra autenticación en /api/deployer, POST /api/modules, /api/stats y /verify (assertIT + validación de slugs/nombres). GET /api/modules público sanitizado (solo visibles, sin repoUrl ni logs). Corrige estado unhealthy, XSS en plantillas, open redirect en login. Sesión HMAC en tiempo constante, secret obligatorio, 503 si el backend cae, cookie secure solo en prod. Migra middleware a proxy, rate-limit en login, vitest + typecheck, Tailwind/CDN fuera, Boxicons local, polling pausado y código muerto eliminado. Estandariza pnpm (lock, Dockerfile, packageManager).
2026-10-08 10:29:13 -03:00

105 lines
2.9 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { assertIT } from "@/lib/correo-server";
const DEPLOYER_URL = process.env.DEPLOYER_URL || "http://127.0.0.1:3010";
const DEPLOYER_TOKEN = process.env.DEPLOYER_TOKEN || "";
// Solo segmentos seguros: evita path traversal (.., /, %, etc.)
const SEG_RE = /^[a-z0-9][a-z0-9_-]*$/i;
function badRequest(msg: string) {
return NextResponse.json({ error: msg }, { status: 400 });
}
// Proxy genérico al deployer: /api/deployer/<path>?<query>
// Métodos: GET, POST, PUT, PATCH, DELETE. Requiere sesión IT.
async function proxy(req: NextRequest, path: string[]) {
try {
await assertIT(req);
} catch (e) {
const status =
typeof e === "object" && e !== null && "status" in e
? Number((e as { status: unknown }).status) || 401
: 401;
return NextResponse.json({ error: "Requiere sesión IT" }, { status });
}
if (!Array.isArray(path) || path.length === 0 || path.length > 8) {
return badRequest("Ruta inválida");
}
for (const seg of path) {
if (!seg || seg.length > 64 || seg === "." || seg === ".." || !SEG_RE.test(seg)) {
return badRequest("Ruta inválida");
}
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (DEPLOYER_TOKEN) headers["X-API-Token"] = DEPLOYER_TOKEN;
try {
const qs = req.nextUrl.search || "";
const init: RequestInit = {
method: req.method,
headers,
cache: "no-store",
};
if (req.method !== "GET" && req.method !== "DELETE") {
try {
init.body = JSON.stringify(await req.json());
} catch {
init.body = undefined;
}
}
const res = await fetch(
`${DEPLOYER_URL}/api/${path.join("/")}${qs}`,
init
);
const text = await res.text();
try {
return NextResponse.json(JSON.parse(text), { status: res.status });
} catch {
return new NextResponse(text, { status: res.status });
}
} catch {
console.error("[deployer-proxy] error al conectar con deployer");
return NextResponse.json(
{ error: "Error al conectar con deployer" },
{ status: 502 }
);
}
}
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
) {
return proxy(req, (await params).slug);
}
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
) {
return proxy(req, (await params).slug);
}
export async function PUT(
req: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
) {
return proxy(req, (await params).slug);
}
export async function PATCH(
req: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
) {
return proxy(req, (await params).slug);
}
export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
) {
return proxy(req, (await params).slug);
}