Add EpicNext CMS foundation layer and fix critical security gaps
Local Build and Deploy / deploy (push) Successful in 1m1s

- 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
This commit is contained in:
openhands committed 2026-07-13 12:03:49 +02:00
1 parent 8bf1aa2fa7
commit f6ad030c5b
16 files changed
+1012 -96

No files matched your search

+5 -5
View File
@@ -3,8 +3,8 @@
import { z } from "zod";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { adminAction, actionOk } from "@/lib/foundation/action";
import { NotFoundError } from "@/lib/foundation/errors";
import { logAudit } from "@/lib/services/audit";
import { rcon } from "@/lib/services/rcon";
@@ -16,7 +16,7 @@ export const assignCfhTicket = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: cfhIdSchema },
async (ctx) => {
const ticket = await prisma.supportTickets.findUnique({ where: { id: ctx.data.ticketId } });
if (!ticket) throw new ActionError("Ticket not found");
if (!ticket) throw new NotFoundError("SupportTicket", ctx.data.ticketId);
await prisma.supportTickets.update({
where: { id: ctx.data.ticketId },
@@ -43,7 +43,7 @@ export const updateCfhState = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: cfhStateSchema },
async (ctx) => {
const ticket = await prisma.supportTickets.findUnique({ where: { id: ctx.data.ticketId } });
if (!ticket) throw new ActionError("Ticket not found");
if (!ticket) throw new NotFoundError("SupportTicket", ctx.data.ticketId);
await prisma.supportTickets.update({
where: { id: ctx.data.ticketId },
@@ -104,7 +104,7 @@ export const quickKick = adminAction(
const muteSchema = z.object({
userId: z.coerce.number().int().positive(),
duration: z.coerce.number().int().min(0).max(525600), // max 1 year in minutes
duration: z.coerce.number().int().min(0).max(525600),
});
export const quickMute = adminAction(