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

+118
View File
@@ -0,0 +1,118 @@
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import { PrismaClient } from "@/generated/prisma/client";
import { env } from "@/env";
import { logger } from "@/lib/logger";
import { DatabaseError } from "./errors";
import { getRequestId } from "./request-context";
const globalForDb = globalThis as unknown as { _db?: DbService };
interface HealthStatus {
ok: boolean;
latencyMs: number;
poolSize: number;
activeQueries: number;
error?: string;
}
export class DbService {
private readonly client: PrismaClient;
private queryCount = 0;
private lastHealthCheck = 0;
private healthCache: HealthStatus | null = null;
private readonly healthTtlMs = 10_000;
constructor() {
const url = new URL(env.DATABASE_URL);
const adapter = new PrismaMariaDb({
host: url.hostname,
port: Number(url.port) || 3306,
user: decodeURIComponent(url.username),
password: decodeURIComponent(url.password),
database: url.pathname.replace(/^\//, ""),
connectionLimit: env.DATABASE_POOL_SIZE,
connectTimeout: env.DATABASE_CONNECT_TIMEOUT_MS,
acquireTimeout: env.DATABASE_CONNECT_TIMEOUT_MS,
idleTimeout: env.DATABASE_IDLE_TIMEOUT_MS,
});
this.client = new PrismaClient({
adapter,
log: env.NODE_ENV === "development" ? [{ emit: "event", level: "query" }, { emit: "event", level: "error" }] : [{ emit: "event", level: "error" }],
});
if (env.NODE_ENV === "development") {
this.client.$on("query" as never, (e: unknown) => {
const ev = e as { query: string; duration: number };
logger.debug("DB query", { query: ev.query.slice(0, 200), durationMs: ev.duration, requestId: getRequestId() });
});
}
this.client.$on("error" as never, (e: unknown) => {
const ev = e as { message: string };
logger.error("DB error", { message: ev.message, requestId: getRequestId() });
});
}
get prisma(): PrismaClient {
return this.client;
}
async health(): Promise<HealthStatus> {
const now = Date.now();
if (this.healthCache && now - this.lastHealthCheck < this.healthTtlMs) {
return this.healthCache;
}
const start = performance.now();
try {
await this.client.$queryRaw`SELECT 1`;
const latencyMs = Math.round(performance.now() - start);
this.healthCache = { ok: true, latencyMs, poolSize: env.DATABASE_POOL_SIZE, activeQueries: 0 };
this.lastHealthCheck = now;
return this.healthCache;
} catch (cause) {
const latencyMs = Math.round(performance.now() - start);
const message = cause instanceof Error ? cause.message : "Unknown database error";
this.healthCache = { ok: false, latencyMs, poolSize: env.DATABASE_POOL_SIZE, activeQueries: 0, error: message };
this.lastHealthCheck = now;
return this.healthCache;
}
}
async execute<T>(fn: (client: PrismaClient) => Promise<T>): Promise<T> {
this.queryCount++;
try {
return await fn(this.client);
} catch (cause) {
throw new DatabaseError("Query failed", cause);
}
}
async transaction<T>(fn: (tx: Omit<PrismaClient, "$connect" | "$disconnect" | "$on" | "$use" | "$extends">) => Promise<T>): Promise<T> {
try {
return await this.client.$transaction(fn);
} catch (cause) {
throw new DatabaseError("Transaction failed", cause);
}
}
async rawQuery<T>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T> {
try {
return await this.client.$queryRaw<T>(strings, ...values);
} catch (cause) {
throw new DatabaseError("Raw query failed", cause);
}
}
async executeRaw(query: string, ...values: unknown[]): Promise<number> {
try {
return await this.client.$executeRawUnsafe(query, ...values);
} catch (cause) {
throw new DatabaseError("Execute raw failed", cause);
}
}
}
export const db = globalForDb._db ?? (globalForDb._db = new DbService());
if (env.NODE_ENV !== "production") globalForDb._db = db;