Files
Epicnabbo-Catalogus-Updated…/src/lib/api.ts
T
openhands 5628e7d6b7 Security hardening: 12 improvements across the stack
1. env.ts: APP_KEY placeholder detection with validation
2. schema.prisma: password column widened to varchar(255) for argon2id
3. auth.ts: trustHost restricted to development only
4. next.config.ts: added CSP, HSTS, X-Frame-Options, and other security headers
5. api.ts: CORS restricted to APP_URL instead of wildcard
6. register-form.tsx: migrated from REST API fetch to server action (useActionState)
7. twofactor.ts + 2fa page: TOTP recovery codes (8 one-time codes, generated and displayed)
8. register.ts: password min length 8 + complexity requirements (upper, lower, digit)
9. register.ts + help-tickets.ts + radio-shouts.ts: Zod schema validation
10. rate-limit.ts: improved periodic cleanup with aggressive eviction at 10k buckets
11. guard.ts + admin actions: rate-limited admin actions (30 req/min per staff)
12. help-tickets.ts + radio-shouts.ts: content moderation via moderateOrThrow
2026-07-04 18:52:00 +02:00

34 lines
1.3 KiB
TypeScript

import { NextResponse } from "next/server";
/**
* JSON response helper for the public REST API. Serialises BigInt (Prisma ids)
* to strings — JSON.stringify throws on BigInt otherwise — and sets CORS to
* APP_URL so the game client / external integrations can read it.
*/
const CORS_ORIGIN = process.env.APP_URL ?? "http://localhost:3000";
export function apiJson(data: unknown, init?: ResponseInit): NextResponse {
const body = JSON.stringify(data, (_k, v) => (typeof v === "bigint" ? v.toString() : v));
return new NextResponse(body, {
status: init?.status ?? 200,
headers: {
"content-type": "application/json; charset=utf-8",
"access-control-allow-origin": CORS_ORIGIN,
"cache-control": "no-store",
...(init?.headers ?? {}),
},
});
}
/** Standard error envelope. */
export function apiError(message: string, status = 400): NextResponse {
return apiJson({ error: message }, { status });
}
/** Clamp a ?page / ?perPage pair from search params. */
export function pagination(searchParams: URLSearchParams, defaultPer = 20, maxPer = 100) {
const page = Math.max(1, Number(searchParams.get("page") ?? "1") || 1);
const perPage = Math.min(maxPer, Math.max(1, Number(searchParams.get("perPage") ?? defaultPer) || defaultPer));
return { page, perPage, skip: (page - 1) * perPage, take: perPage };
}