|
|
|
|
@@ -2,126 +2,207 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
|
|
|
|
|
|
|
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://127.0.0.1:3000';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Catch-all proxy: forwards ALL requests to the backend API.
|
|
|
|
|
* This avoids CORS preflight issues since requests stay within the
|
|
|
|
|
* same origin (localhost:3004 -> localhost:3004 proxy -> 127.0.0.1:3000 backend).
|
|
|
|
|
*
|
|
|
|
|
* More specific routes (e.g. /api/auth/login) take precedence in Next.js,
|
|
|
|
|
* so they are NOT served by this handler.
|
|
|
|
|
*/
|
|
|
|
|
export async function GET(req: NextRequest) {
|
|
|
|
|
const path = req.nextUrl.pathname.replace('/api/', '');
|
|
|
|
|
const search = req.nextUrl.search;
|
|
|
|
|
const cookies = req.headers.get('cookie') ?? '';
|
|
|
|
|
try {
|
|
|
|
|
const backendRes = await fetch(`${API}/${path}${search}`, {
|
|
|
|
|
headers: { Cookie: cookies },
|
|
|
|
|
});
|
|
|
|
|
if (path === 'admin/logs/stream') {
|
|
|
|
|
// Pipe SSE directly: Next.js App Router supports ReadableStream passthrough.
|
|
|
|
|
// X-Accel-Buffering: no tells any intermediate proxy (nginx) not to buffer.
|
|
|
|
|
const headers = new Headers();
|
|
|
|
|
headers.set('Content-Type', backendRes.headers.get('content-type') ?? 'text/event-stream');
|
|
|
|
|
headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
|
|
|
headers.set('X-Accel-Buffering', 'no');
|
|
|
|
|
// Do NOT set Connection: keep-alive — it is HTTP/1.1 default for persistent
|
|
|
|
|
// connections and can confuse proxies that do not expect streaming.
|
|
|
|
|
return new Response(backendRes.body, {
|
|
|
|
|
status: backendRes.status,
|
|
|
|
|
headers,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const data = await backendRes.json().catch(() => null);
|
|
|
|
|
const resp = NextResponse.json(data ?? { error: 'Bad response' }, { status: backendRes.status });
|
|
|
|
|
return resp;
|
|
|
|
|
} catch {
|
|
|
|
|
return NextResponse.json({ error: 'Proxy error' }, { status: 502 });
|
|
|
|
|
}
|
|
|
|
|
const PROXY_TIMEOUT_MS = 25_000;
|
|
|
|
|
const REQUEST_ID_HEADER = 'x-request-id';
|
|
|
|
|
|
|
|
|
|
interface ProxyInit {
|
|
|
|
|
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
|
|
|
/** Raw body, or `null` when there is no body to forward. */
|
|
|
|
|
body: string | null;
|
|
|
|
|
headers: Record<string, string>;
|
|
|
|
|
requestId: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function POST(req: NextRequest) {
|
|
|
|
|
const path = req.nextUrl.pathname.replace('/api/', '');
|
|
|
|
|
const search = req.nextUrl.search;
|
|
|
|
|
const cookies = req.headers.get('cookie') ?? '';
|
|
|
|
|
const body = await req.text();
|
|
|
|
|
interface ProxyResult {
|
|
|
|
|
status: number;
|
|
|
|
|
headers: Headers;
|
|
|
|
|
body: ReadableStream | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function proxyToBackend(
|
|
|
|
|
request: NextRequest,
|
|
|
|
|
init: ProxyInit,
|
|
|
|
|
): Promise<ProxyResult> {
|
|
|
|
|
const path = request.nextUrl.pathname.replace('/api/', '');
|
|
|
|
|
const search = request.nextUrl.search;
|
|
|
|
|
const url = `${API}/${path}${search}`;
|
|
|
|
|
|
|
|
|
|
// Forward only the cookies the proxy knows are safe to relay. We deliberately
|
|
|
|
|
// do NOT pass `content-length` (Node fetch sets it from `body`), and we keep
|
|
|
|
|
// `content-type` only when we actually have a body to send — otherwise some
|
|
|
|
|
// upstreams reject the request or return 502 on bodyless PATCH/POST calls.
|
|
|
|
|
const headers: Record<string, string> = {
|
|
|
|
|
Cookie: init.headers.Cookie ?? '',
|
|
|
|
|
[REQUEST_ID_HEADER]: init.requestId,
|
|
|
|
|
};
|
|
|
|
|
if (init.body !== null) {
|
|
|
|
|
headers['Content-Type'] = 'application/json';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
const timeout = setTimeout(() => controller.abort(), PROXY_TIMEOUT_MS);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const headers: Record<string, string> = { Cookie: cookies };
|
|
|
|
|
if (body) headers['Content-Type'] = 'application/json';
|
|
|
|
|
const backendRes = await fetch(`${API}/${path}${search}`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
const backendRes = await fetch(url, {
|
|
|
|
|
method: init.method,
|
|
|
|
|
headers,
|
|
|
|
|
...(body ? { body } : {}),
|
|
|
|
|
body: init.body,
|
|
|
|
|
signal: controller.signal,
|
|
|
|
|
});
|
|
|
|
|
const responseHeaders = new Headers();
|
|
|
|
|
const contentType = backendRes.headers.get('content-type');
|
|
|
|
|
if (contentType) responseHeaders.set('Content-Type', contentType);
|
|
|
|
|
const setCookie = backendRes.headers.get('set-cookie');
|
|
|
|
|
const data = await backendRes.json().catch(() => null);
|
|
|
|
|
const resp = NextResponse.json(data ?? { error: 'Bad response' }, { status: backendRes.status });
|
|
|
|
|
if (setCookie) {
|
|
|
|
|
resp.headers.set(
|
|
|
|
|
responseHeaders.set(
|
|
|
|
|
'Set-Cookie',
|
|
|
|
|
setCookie.replace(/;\s*Secure/gi, '').replace(/;\s*SameSite=Lax/gi, '').trim(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return resp;
|
|
|
|
|
} catch {
|
|
|
|
|
return NextResponse.json({ error: 'Proxy error' }, { status: 502 });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function PATCH(req: NextRequest) {
|
|
|
|
|
const path = req.nextUrl.pathname.replace('/api/', '');
|
|
|
|
|
const search = req.nextUrl.search;
|
|
|
|
|
const cookies = req.headers.get('cookie') ?? '';
|
|
|
|
|
const body = await req.text();
|
|
|
|
|
try {
|
|
|
|
|
const backendRes = await fetch(`${API}/${path}${search}`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', Cookie: cookies },
|
|
|
|
|
body,
|
|
|
|
|
});
|
|
|
|
|
const data = await backendRes.json().catch(() => null);
|
|
|
|
|
return NextResponse.json(data ?? { error: 'Bad response' }, { status: backendRes.status });
|
|
|
|
|
} catch {
|
|
|
|
|
return NextResponse.json({ error: 'Proxy error' }, { status: 502 });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function PUT(req: NextRequest) {
|
|
|
|
|
const path = req.nextUrl.pathname.replace('/api/', '');
|
|
|
|
|
const search = req.nextUrl.search;
|
|
|
|
|
const cookies = req.headers.get('cookie') ?? '';
|
|
|
|
|
const body = await req.text();
|
|
|
|
|
try {
|
|
|
|
|
const backendRes = await fetch(`${API}/${path}${search}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', Cookie: cookies },
|
|
|
|
|
body,
|
|
|
|
|
});
|
|
|
|
|
const data = await backendRes.json().catch(() => null);
|
|
|
|
|
return NextResponse.json(data ?? { error: 'Bad response' }, { status: backendRes.status });
|
|
|
|
|
} catch {
|
|
|
|
|
return NextResponse.json({ error: 'Proxy error' }, { status: 502 });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function DELETE(req: NextRequest) {
|
|
|
|
|
const path = req.nextUrl.pathname.replace('/api/', '');
|
|
|
|
|
const search = req.nextUrl.search;
|
|
|
|
|
const cookies = req.headers.get('cookie') ?? '';
|
|
|
|
|
try {
|
|
|
|
|
const backendRes = await fetch(`${API}/${path}${search}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: { Cookie: cookies },
|
|
|
|
|
});
|
|
|
|
|
// Reenviar la respuesta del backend tal cual: el backend puede devolver
|
|
|
|
|
// 204 No Content (sin body) en borrados exitosos, y forzar un JSON con
|
|
|
|
|
// status 204 es HTTP inválido (F-127).
|
|
|
|
|
return new Response(backendRes.body, {
|
|
|
|
|
return {
|
|
|
|
|
status: backendRes.status,
|
|
|
|
|
statusText: backendRes.statusText,
|
|
|
|
|
headers: { 'Content-Length': backendRes.headers.get('content-length') ?? '0' },
|
|
|
|
|
});
|
|
|
|
|
} catch {
|
|
|
|
|
return NextResponse.json({ error: 'Proxy error' }, { status: 502 });
|
|
|
|
|
headers: responseHeaders,
|
|
|
|
|
body: backendRes.body,
|
|
|
|
|
};
|
|
|
|
|
} finally {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reads the incoming request body safely. Returns `null` for bodyless methods
|
|
|
|
|
* (GET/DELETE) and an empty string when the body is empty.
|
|
|
|
|
*/
|
|
|
|
|
async function safeReadBody(request: NextRequest): Promise<string | null> {
|
|
|
|
|
if (request.method === 'GET' || request.method === 'DELETE' || request.method === 'HEAD') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return await request.text();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('[proxy] failed to read body', { message: (error as Error)?.message });
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getRequestId(request: NextRequest): string {
|
|
|
|
|
return (
|
|
|
|
|
request.headers.get(REQUEST_ID_HEADER) ??
|
|
|
|
|
request.headers.get('x-vercel-id') ??
|
|
|
|
|
`adm-${Math.random().toString(36).slice(2, 10)}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function logProxyFailure(stage: string, request: NextRequest, requestId: string, error: unknown) {
|
|
|
|
|
console.warn('[proxy] failure', {
|
|
|
|
|
stage,
|
|
|
|
|
requestId,
|
|
|
|
|
method: request.method,
|
|
|
|
|
target: request.nextUrl.pathname,
|
|
|
|
|
message: error instanceof Error ? error.message : String(error),
|
|
|
|
|
name: error instanceof Error ? error.name : 'unknown',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function GET(request: NextRequest) {
|
|
|
|
|
const requestId = getRequestId(request);
|
|
|
|
|
try {
|
|
|
|
|
const result = await proxyToBackend(request, {
|
|
|
|
|
method: 'GET',
|
|
|
|
|
body: null,
|
|
|
|
|
headers: { Cookie: request.headers.get('cookie') ?? '' },
|
|
|
|
|
requestId,
|
|
|
|
|
});
|
|
|
|
|
if (request.nextUrl.pathname === 'admin/logs/stream') {
|
|
|
|
|
result.headers.set('Content-Type', result.headers.get('content-type') ?? 'text/event-stream');
|
|
|
|
|
result.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
|
|
|
result.headers.set('X-Accel-Buffering', 'no');
|
|
|
|
|
}
|
|
|
|
|
return new Response(result.body, { status: result.status, headers: result.headers });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logProxyFailure('get', request, requestId, error);
|
|
|
|
|
return NextResponse.json(
|
|
|
|
|
{ error: { code: 'PROXY_ERROR', message: 'Proxy error', requestId } },
|
|
|
|
|
{ status: 502 },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function POST(request: NextRequest) {
|
|
|
|
|
const requestId = getRequestId(request);
|
|
|
|
|
try {
|
|
|
|
|
const body = await safeReadBody(request);
|
|
|
|
|
const result = await proxyToBackend(request, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body,
|
|
|
|
|
headers: { Cookie: request.headers.get('cookie') ?? '' },
|
|
|
|
|
requestId,
|
|
|
|
|
});
|
|
|
|
|
return new Response(result.body, { status: result.status, headers: result.headers });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logProxyFailure('post', request, requestId, error);
|
|
|
|
|
return NextResponse.json(
|
|
|
|
|
{ error: { code: 'PROXY_ERROR', message: 'Proxy error', requestId } },
|
|
|
|
|
{ status: 502 },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function PATCH(request: NextRequest) {
|
|
|
|
|
const requestId = getRequestId(request);
|
|
|
|
|
try {
|
|
|
|
|
const body = await safeReadBody(request);
|
|
|
|
|
const result = await proxyToBackend(request, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
body,
|
|
|
|
|
headers: { Cookie: request.headers.get('cookie') ?? '' },
|
|
|
|
|
requestId,
|
|
|
|
|
});
|
|
|
|
|
return new Response(result.body, { status: result.status, headers: result.headers });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logProxyFailure('patch', request, requestId, error);
|
|
|
|
|
return NextResponse.json(
|
|
|
|
|
{ error: { code: 'PROXY_ERROR', message: 'Proxy error', requestId } },
|
|
|
|
|
{ status: 502 },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function PUT(request: NextRequest) {
|
|
|
|
|
const requestId = getRequestId(request);
|
|
|
|
|
try {
|
|
|
|
|
const body = await safeReadBody(request);
|
|
|
|
|
const result = await proxyToBackend(request, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
body,
|
|
|
|
|
headers: { Cookie: request.headers.get('cookie') ?? '' },
|
|
|
|
|
requestId,
|
|
|
|
|
});
|
|
|
|
|
return new Response(result.body, { status: result.status, headers: result.headers });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logProxyFailure('put', request, requestId, error);
|
|
|
|
|
return NextResponse.json(
|
|
|
|
|
{ error: { code: 'PROXY_ERROR', message: 'Proxy error', requestId } },
|
|
|
|
|
{ status: 502 },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function DELETE(request: NextRequest) {
|
|
|
|
|
const requestId = getRequestId(request);
|
|
|
|
|
try {
|
|
|
|
|
const result = await proxyToBackend(request, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
body: null,
|
|
|
|
|
headers: { Cookie: request.headers.get('cookie') ?? '' },
|
|
|
|
|
requestId,
|
|
|
|
|
});
|
|
|
|
|
return new Response(result.body, { status: result.status, headers: result.headers });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logProxyFailure('delete', request, requestId, error);
|
|
|
|
|
return NextResponse.json(
|
|
|
|
|
{ error: { code: 'PROXY_ERROR', message: 'Proxy error', requestId } },
|
|
|
|
|
{ status: 502 },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|