Files
EpicNext-Cms/src/lib/api-handler.ts
T
openhands f6ad030c5b
Local Build and Deploy / deploy (push) Successful in 1m1s
Add EpicNext CMS foundation layer and fix critical security gaps
- Create src/lib/foundation/ (860 LOC, 9 files): typed action wrappers,
  DbService with health checks, CSRF validation, safe redirects,
  AsyncLocalStorage request tracing, branded types, reusable Zod schemas
- Migrate moderation.ts and user-settings.ts to foundation patterns
- Fix abuse-guard.ts: bound in-memory Maps with LRU eviction (was unbounded)
- Fix access-guard.ts: separate try/catch per check, log degradation
  instead of blanket fail-open
- Replace raw redirect() calls with safeRedirect() in guard.ts and
  permissions.ts to prevent open-redirect attacks
- Add CSRF validation to api-handler.ts for mutating methods
- Add canonicalizeFormData() utility for FormData input sanitization
2026-07-13 12:03:49 +02:00

56 lines
2.3 KiB
TypeScript

import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { logAuthorizationEvent } from "@/lib/admin/authorization-events";
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"]);
type AdminContext = NonNullable<Awaited<ReturnType<typeof getApiAdminContext>>>;
type RouteContext = { params?: Promise<Record<string, string | string[]>> };
type AdminHandler = (
request: NextRequest,
context: AdminContext,
routeContext: RouteContext,
) => Promise<Response> | Response;
export function withAdmin(options: { permission?: string; requireCsrf?: boolean }, 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 });
}
}
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 });
}
};
}