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) } } }