import type { z } from "zod"; import { auth } from "@/lib/auth"; import { canAccess, getApiAdminContext } from "@/lib/permissions"; import { type ActionResult, actionError, handleActionError } from "@/lib/safe-action-shared"; import { logAuthorizationEvent } from "@/lib/admin/authorization-events"; export type { ActionResult }; // ── Admin action wrapper ───────────────────────────────────────────── interface AdminActionOptions { permission?: string; schema?: TSchema; } type AdminActionContext = { session: { user: { id: number; username: string; rank: number; look: string; mail: string } }; } & (TSchema extends z.ZodType ? { data: z.infer } : object); /** * Create a server action with admin auth + optional permission + optional Zod validation. * * @example * export const updateNews = adminAction( * { permission: PERMS.NEWS_EDIT, schema: updateNewsSchema }, * async (ctx) => { * await prisma.websiteArticle.update({ ... }) * return actionOk() * } * ) */ export function adminAction( options: AdminActionOptions, handler: (ctx: AdminActionContext) => Promise, ) { return async ( // biome-ignore lint/suspicious/noConfusingVoidType: void in conditional return lets callers omit the arg when there's no schema input: TSchema extends z.ZodType ? z.input : void, ): Promise => { try { const apiCtx = await getApiAdminContext(); if (!apiCtx) return actionError("Unauthorized"); if (options.permission) { if (!canAccess(apiCtx.permissions, options.permission, apiCtx.session.user.rank)) { await logAuthorizationEvent({ kind: "permission.denied", userId: apiCtx.session.user.id, username: apiCtx.session.user.name ?? undefined, rank: apiCtx.session.user.rank, permission: options.permission, source: "adminAction", reason: "Permission check denied", }); return actionError("Unauthorized"); } } let data: unknown; if (options.schema) { const parsed = options.schema.safeParse(input); if (!parsed.success) { return { ok: false, error: "Validation failed", fieldErrors: parsed.error.flatten().fieldErrors as Record, }; } data = parsed.data; } const ctx = { session: apiCtx.session, ...(options.schema ? { data } : {}), } as AdminActionContext; return await handler(ctx); } catch (error) { return handleActionError(error); } }; } // ── Auth action wrapper (no permissions) ───────────────────────────── interface AuthActionOptions { schema?: TSchema; } type AuthActionContext = { session: { user: { id: number; username: string; rank: number; look: string; mail: string } }; } & (TSchema extends z.ZodType ? { data: z.infer } : object); /** * Create a server action with auth only (no permission check). */ export function authAction( options: AuthActionOptions, handler: (ctx: AuthActionContext) => Promise, ) { return async ( // biome-ignore lint/suspicious/noConfusingVoidType: void in conditional return lets callers omit the arg when there's no schema input: TSchema extends z.ZodType ? z.input : void, ): Promise => { try { const session = await auth(); if (!session?.user) return actionError("Unauthorized"); let data: unknown; if (options.schema) { const parsed = options.schema.safeParse(input); if (!parsed.success) { return { ok: false, error: "Validation failed", fieldErrors: parsed.error.flatten().fieldErrors as Record, }; } data = parsed.data; } const ctx = { session, ...(options.schema ? { data } : {}), } as AuthActionContext; return await handler(ctx); } catch (error) { return handleActionError(error); } }; }