This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
+75
-51
@@ -1,9 +1,9 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { logAuthorizationEvent } from "@/lib/admin/authorization-events";
|
||||
import { validateCsrfToken } from "@/lib/foundation/security";
|
||||
import { canAccess, getApiAdminContext } from "@/lib/permissions";
|
||||
import { logServerError } from "@/lib/server-log";
|
||||
import { validateCsrfToken } from "@/lib/foundation/security";
|
||||
|
||||
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
||||
const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB
|
||||
@@ -11,60 +11,84 @@ const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB
|
||||
type AdminContext = NonNullable<Awaited<ReturnType<typeof getApiAdminContext>>>;
|
||||
type RouteContext = { params?: Promise<Record<string, string | string[]>> };
|
||||
type AdminHandler = (
|
||||
request: NextRequest,
|
||||
context: AdminContext,
|
||||
routeContext: RouteContext,
|
||||
request: NextRequest,
|
||||
context: AdminContext,
|
||||
routeContext: RouteContext,
|
||||
) => Promise<Response> | Response;
|
||||
|
||||
export function withAdmin(
|
||||
options: { permission?: string; requireCsrf?: boolean; maxBodyBytes?: number },
|
||||
handler: AdminHandler,
|
||||
options: {
|
||||
permission?: string;
|
||||
requireCsrf?: boolean;
|
||||
maxBodyBytes?: number;
|
||||
},
|
||||
handler: AdminHandler,
|
||||
) {
|
||||
return async (request: NextRequest, routeContext: RouteContext = {}) => {
|
||||
if (options.requireCsrf === true && MUTATING_METHODS.has(request.method)) {
|
||||
const csrfToken = request.headers.get("x-csrf-token") ?? request.headers.get("csrf-token") ?? "";
|
||||
const valid = await validateCsrfToken(csrfToken);
|
||||
if (!valid) {
|
||||
return NextResponse.json({ ok: false, error: "Invalid or missing CSRF token" }, { status: 403 });
|
||||
}
|
||||
}
|
||||
return async (request: NextRequest, routeContext: RouteContext = {}) => {
|
||||
if (options.requireCsrf === true && MUTATING_METHODS.has(request.method)) {
|
||||
const csrfToken =
|
||||
request.headers.get("x-csrf-token") ??
|
||||
request.headers.get("csrf-token") ??
|
||||
"";
|
||||
const valid = await validateCsrfToken(csrfToken);
|
||||
if (!valid) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Invalid or missing CSRF token" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (MUTATING_METHODS.has(request.method)) {
|
||||
const contentLength = request.headers.get("content-length");
|
||||
const maxBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
||||
if (contentLength && Number(contentLength) > maxBytes) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: `Request body exceeds ${maxBytes} bytes` },
|
||||
{ status: 413 },
|
||||
);
|
||||
}
|
||||
}
|
||||
if (MUTATING_METHODS.has(request.method)) {
|
||||
const contentLength = request.headers.get("content-length");
|
||||
const maxBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
||||
if (contentLength && Number(contentLength) > maxBytes) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: `Request body exceeds ${maxBytes} bytes` },
|
||||
{ status: 413 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const context = await getApiAdminContext();
|
||||
if (!context) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||
if (
|
||||
options.permission &&
|
||||
!canAccess(context.permissions, options.permission, context.session.user.rank)
|
||||
) {
|
||||
await logAuthorizationEvent({
|
||||
kind: "permission.denied",
|
||||
userId: context.session.user.id,
|
||||
username: context.session.user.username,
|
||||
rank: context.session.user.rank,
|
||||
permission: options.permission,
|
||||
source: request.nextUrl.pathname,
|
||||
reason: "API permission check denied",
|
||||
});
|
||||
return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
try {
|
||||
return await handler(request, context, routeContext);
|
||||
} catch (error) {
|
||||
logServerError("admin.api_failed", error, {
|
||||
path: request.nextUrl.pathname,
|
||||
userId: context.session.user.id,
|
||||
});
|
||||
return NextResponse.json({ ok: false, error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
};
|
||||
const context = await getApiAdminContext();
|
||||
if (!context)
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Unauthorized" },
|
||||
{ status: 401 },
|
||||
);
|
||||
if (
|
||||
options.permission &&
|
||||
!canAccess(
|
||||
context.permissions,
|
||||
options.permission,
|
||||
context.session.user.rank,
|
||||
)
|
||||
) {
|
||||
await logAuthorizationEvent({
|
||||
kind: "permission.denied",
|
||||
userId: context.session.user.id,
|
||||
username: context.session.user.username,
|
||||
rank: context.session.user.rank,
|
||||
permission: options.permission,
|
||||
source: request.nextUrl.pathname,
|
||||
reason: "API permission check denied",
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Forbidden" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
try {
|
||||
return await handler(request, context, routeContext);
|
||||
} catch (error) {
|
||||
logServerError("admin.api_failed", error, {
|
||||
path: request.nextUrl.pathname,
|
||||
userId: context.session.user.id,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user