249 أسطر
6.8 KiB
TypeScript
249 أسطر
6.8 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? process.env.API_BASE_URL ?? "";
|
|
const ACCESS_COOKIE = "oudelaa_sa_access";
|
|
const REFRESH_COOKIE = "oudelaa_sa_refresh";
|
|
const ACCESS_MAX_AGE_SECONDS = 15 * 60;
|
|
const REFRESH_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
|
|
function buildTargetUrl(pathSegments: string[], searchParams: URLSearchParams) {
|
|
const base = API_BASE_URL.replace(/\/$/, "");
|
|
const path = pathSegments.join("/");
|
|
const query = searchParams.toString();
|
|
return `${base}/${path}${query ? `?${query}` : ""}`;
|
|
}
|
|
|
|
function isSuperAdminLogin(pathSegments: string[]) {
|
|
return pathSegments.join("/") === "auth/superadmin/login";
|
|
}
|
|
|
|
function isSuperAdminRefresh(pathSegments: string[]) {
|
|
return pathSegments.join("/") === "auth/superadmin/refresh";
|
|
}
|
|
|
|
function isSuperAdminLogout(pathSegments: string[]) {
|
|
return pathSegments.join("/") === "auth/superadmin/logout";
|
|
}
|
|
|
|
function isJsonContent(contentType: string | null) {
|
|
return (contentType ?? "").toLowerCase().includes("application/json");
|
|
}
|
|
|
|
function parseJsonSafe<T>(value: string): T | null {
|
|
try {
|
|
return JSON.parse(value) as T;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function buildUpstreamHeaders(params: {
|
|
req: NextRequest;
|
|
pathSegments: string[];
|
|
accessToken?: string;
|
|
contentType: string | null;
|
|
}) {
|
|
const headers = new Headers();
|
|
const { req, pathSegments, accessToken, contentType } = params;
|
|
|
|
const accept = req.headers.get("accept");
|
|
if (accept) {
|
|
headers.set("accept", accept);
|
|
}
|
|
|
|
if (contentType) {
|
|
headers.set("content-type", contentType);
|
|
}
|
|
|
|
const authorization = req.headers.get("authorization");
|
|
if (authorization) {
|
|
headers.set("authorization", authorization);
|
|
} else if (accessToken && !isSuperAdminLogin(pathSegments)) {
|
|
headers.set("authorization", `Bearer ${accessToken}`);
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
function applyAuthCookies(response: NextResponse, payload: {
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
}) {
|
|
const secure = process.env.NODE_ENV === "production";
|
|
|
|
if (payload.accessToken) {
|
|
response.cookies.set(ACCESS_COOKIE, payload.accessToken, {
|
|
httpOnly: true,
|
|
secure,
|
|
sameSite: "lax",
|
|
path: "/",
|
|
maxAge: ACCESS_MAX_AGE_SECONDS,
|
|
});
|
|
}
|
|
|
|
if (payload.refreshToken) {
|
|
response.cookies.set(REFRESH_COOKIE, payload.refreshToken, {
|
|
httpOnly: true,
|
|
secure,
|
|
sameSite: "lax",
|
|
path: "/",
|
|
maxAge: REFRESH_MAX_AGE_SECONDS,
|
|
});
|
|
}
|
|
}
|
|
|
|
function clearAuthCookies(response: NextResponse) {
|
|
response.cookies.set(ACCESS_COOKIE, "", {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
sameSite: "lax",
|
|
path: "/",
|
|
maxAge: 0,
|
|
});
|
|
response.cookies.set(REFRESH_COOKIE, "", {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
sameSite: "lax",
|
|
path: "/",
|
|
maxAge: 0,
|
|
});
|
|
}
|
|
|
|
async function proxyRequest(req: NextRequest, pathSegments: string[] = []) {
|
|
if (!API_BASE_URL) {
|
|
return new NextResponse("Missing API base URL", { status: 500 });
|
|
}
|
|
|
|
const url = new URL(req.url);
|
|
const targetUrl = buildTargetUrl(pathSegments, url.searchParams);
|
|
const isReadOnlyMethod = ["GET", "HEAD"].includes(req.method);
|
|
const contentType = req.headers.get("content-type");
|
|
const accessToken = req.cookies.get(ACCESS_COOKIE)?.value;
|
|
const refreshToken = req.cookies.get(REFRESH_COOKIE)?.value;
|
|
const headers = buildUpstreamHeaders({
|
|
req,
|
|
pathSegments,
|
|
accessToken,
|
|
contentType,
|
|
});
|
|
|
|
let body: BodyInit | undefined;
|
|
|
|
if (!isReadOnlyMethod) {
|
|
const bodyBuffer = await req.arrayBuffer();
|
|
const hasBody = bodyBuffer.byteLength > 0;
|
|
body = hasBody ? bodyBuffer : undefined;
|
|
|
|
const shouldInjectRefreshToken =
|
|
(isSuperAdminRefresh(pathSegments) || isSuperAdminLogout(pathSegments)) &&
|
|
isJsonContent(contentType);
|
|
|
|
if (shouldInjectRefreshToken) {
|
|
const rawText = hasBody ? Buffer.from(bodyBuffer).toString("utf8") : "";
|
|
const parsed = parseJsonSafe<Record<string, unknown>>(rawText) ?? {};
|
|
if (!parsed.refreshToken && refreshToken) {
|
|
parsed.refreshToken = refreshToken;
|
|
}
|
|
body = JSON.stringify(parsed);
|
|
headers.set("content-type", "application/json");
|
|
}
|
|
}
|
|
|
|
const init: RequestInit = {
|
|
method: req.method,
|
|
headers,
|
|
cache: "no-store",
|
|
} as RequestInit;
|
|
|
|
if (body !== undefined) {
|
|
init.body = body;
|
|
}
|
|
|
|
try {
|
|
const upstream = await fetch(targetUrl, init);
|
|
const responseHeaders = new Headers(upstream.headers);
|
|
responseHeaders.delete("content-encoding");
|
|
responseHeaders.delete("content-length");
|
|
|
|
const isAuthResponse =
|
|
isSuperAdminLogin(pathSegments) ||
|
|
isSuperAdminRefresh(pathSegments) ||
|
|
isSuperAdminLogout(pathSegments);
|
|
|
|
if (isAuthResponse && isJsonContent(upstream.headers.get("content-type"))) {
|
|
const payload = (await upstream.json()) as Record<string, unknown>;
|
|
const response = NextResponse.json(payload, {
|
|
status: upstream.status,
|
|
headers: responseHeaders,
|
|
});
|
|
|
|
if (upstream.ok && (isSuperAdminLogin(pathSegments) || isSuperAdminRefresh(pathSegments))) {
|
|
applyAuthCookies(response, {
|
|
accessToken: typeof payload.accessToken === "string" ? payload.accessToken : undefined,
|
|
refreshToken: typeof payload.refreshToken === "string" ? payload.refreshToken : undefined,
|
|
});
|
|
}
|
|
|
|
if (upstream.ok && isSuperAdminLogout(pathSegments)) {
|
|
clearAuthCookies(response);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
const response = new NextResponse(upstream.body, {
|
|
status: upstream.status,
|
|
headers: responseHeaders,
|
|
});
|
|
|
|
if (upstream.ok && isSuperAdminLogout(pathSegments)) {
|
|
clearAuthCookies(response);
|
|
}
|
|
|
|
return response;
|
|
} catch (error) {
|
|
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
return new NextResponse(`Upstream fetch failed: ${message}`, { status: 502 });
|
|
}
|
|
}
|
|
|
|
export async function GET(
|
|
req: NextRequest,
|
|
context: { params: Promise<{ path: string[] }> },
|
|
) {
|
|
const { path } = await context.params;
|
|
return proxyRequest(req, path ?? []);
|
|
}
|
|
|
|
export async function POST(
|
|
req: NextRequest,
|
|
context: { params: Promise<{ path: string[] }> },
|
|
) {
|
|
const { path } = await context.params;
|
|
return proxyRequest(req, path ?? []);
|
|
}
|
|
|
|
export async function PATCH(
|
|
req: NextRequest,
|
|
context: { params: Promise<{ path: string[] }> },
|
|
) {
|
|
const { path } = await context.params;
|
|
return proxyRequest(req, path ?? []);
|
|
}
|
|
|
|
export async function DELETE(
|
|
req: NextRequest,
|
|
context: { params: Promise<{ path: string[] }> },
|
|
) {
|
|
const { path } = await context.params;
|
|
return proxyRequest(req, path ?? []);
|
|
}
|
|
|
|
export async function PUT(
|
|
req: NextRequest,
|
|
context: { params: Promise<{ path: string[] }> },
|
|
) {
|
|
const { path } = await context.params;
|
|
return proxyRequest(req, path ?? []);
|
|
}
|