phase 2 and 3

This commit is contained in:
jason
2026-04-21 08:56:51 -05:00
parent b98837a72c
commit d79aaf6ef8
42 changed files with 4962 additions and 19 deletions
+73
View File
@@ -0,0 +1,73 @@
import { NextResponse, type NextRequest } from "next/server";
import { ZodError, type ZodSchema } from "zod";
import { getSessionUser, type Role, type SessionUser } from "@/lib/session";
import { clientIp } from "@/lib/request";
export class ApiError extends Error {
constructor(public status: number, public code: string, message: string) {
super(message);
}
}
export function errorResponse(err: unknown): NextResponse {
if (err instanceof ApiError) {
return NextResponse.json({ error: err.message, code: err.code }, { status: err.status });
}
if (err instanceof ZodError) {
return NextResponse.json(
{
error: "Validation failed",
code: "validation_failed",
issues: err.issues.map((i) => ({ path: i.path, message: i.message })),
},
{ status: 400 },
);
}
if (
err &&
typeof err === "object" &&
"code" in err &&
typeof (err as { code: unknown }).code === "string" &&
((err as { code: string }).code === "P2002" || (err as { code: string }).code === "P2003")
) {
const p = err as { code: string; meta?: { target?: unknown } };
const msg =
p.code === "P2002"
? `Duplicate value${p.meta?.target ? ` on ${JSON.stringify(p.meta.target)}` : ""}`
: "Foreign key constraint failed";
return NextResponse.json(
{ error: msg, code: p.code === "P2002" ? "duplicate" : "fk_violation" },
{ status: 409 },
);
}
console.error("[api] unhandled error:", err);
return NextResponse.json(
{ error: "Internal server error", code: "internal" },
{ status: 500 },
);
}
export async function requireRole(role: Role): Promise<SessionUser> {
const user = await getSessionUser();
if (!user) throw new ApiError(401, "unauthenticated", "Sign in required");
if (user.role !== role) throw new ApiError(403, "forbidden", "Not allowed");
return user;
}
export async function parseJson<T>(req: NextRequest, schema: ZodSchema<T>): Promise<T> {
let raw: unknown;
try {
raw = await req.json();
} catch {
throw new ApiError(400, "invalid_json", "Expected JSON body");
}
return schema.parse(raw);
}
export function actorContext(req: NextRequest, user: SessionUser) {
return { actorId: user.id, ipAddress: clientIp(req) };
}
export function ok<T>(data: T, init?: ResponseInit): NextResponse {
return NextResponse.json(data, init);
}