import { NextResponse } from "next/server"; import { z } from "zod"; import { snapshotUser } from "@/lib/audit-snapshots"; import { createAuditLog } from "@/lib/audit-log"; import { APPROVAL_FLOW, canManageUsers, getLegacyApprovalPreference, normalizeApprovalPermissions } from "@/lib/domain"; import prisma from "@/lib/prisma"; import { getCurrentViewer } from "@/lib/session"; const userRoleSchema = z.enum(["ADMIN", "FINANCE", "MEMBER"]); const approvalPermissionSchema = z.enum(APPROVAL_FLOW); const updateUserSchema = z.object({ role: userRoleSchema, workingGroupId: z.union([z.string().trim().min(1), z.literal(""), z.null(), z.undefined()]), approvalPermissions: z.array(approvalPermissionSchema).default([]) }); function serializeManagedUser(user: { id: string; name: string; username: string; role: "ADMIN" | "FINANCE" | "MEMBER"; workingGroupId: string | null; workingGroup: { name: string } | null; approvalPreference: "CHAIR_A" | "CHAIR_B" | "FINANCE" | null; approvalPermissions: ("CHAIR_A" | "CHAIR_B" | "FINANCE")[]; _count: { createdExpenses: number; approvals: number; }; }) { return { id: user.id, name: user.username, username: user.username, role: user.role, workingGroupId: user.workingGroupId, workingGroupName: user.workingGroup?.name ?? null, approvalPermissions: normalizeApprovalPermissions(user.role, user.approvalPermissions, user.approvalPreference), createdExpensesCount: user._count.createdExpenses, approvalsCount: user._count.approvals }; } type Context = { params: Promise<{ id: string; }>; }; export async function PATCH(request: Request, { params }: Context) { const { id } = await params; const viewer = await getCurrentViewer(); if (!viewer) { return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 }); } if (!canManageUsers(viewer.role)) { return NextResponse.json({ error: "Nur Vorstand oder Finanz-AG duerfen Nutzer bearbeiten." }, { status: 403 }); } const body = await request.json().catch(() => null); const parsed = updateUserSchema.safeParse(body); if (!parsed.success) { return NextResponse.json({ error: "Bitte Rolle, AG und Freigaberollen korrekt angeben." }, { status: 400 }); } const user = await prisma.user.findUnique({ where: { id } }); if (!user) { return NextResponse.json({ error: "Nutzer nicht gefunden." }, { status: 404 }); } const workingGroupId = typeof parsed.data.workingGroupId === "string" && parsed.data.workingGroupId.length > 0 ? parsed.data.workingGroupId : null; if (parsed.data.role === "MEMBER" && !workingGroupId) { return NextResponse.json({ error: "AG-Mitglieder brauchen eine AG-Zuordnung." }, { status: 400 }); } if (workingGroupId) { const workingGroup = await prisma.workingGroup.findUnique({ where: { id: workingGroupId } }); if (!workingGroup) { return NextResponse.json({ error: "Die ausgewaehlte AG wurde nicht gefunden." }, { status: 404 }); } } if (user.role === "ADMIN" && parsed.data.role !== "ADMIN") { const adminCount = await prisma.user.count({ where: { role: "ADMIN" } }); if (adminCount <= 1) { return NextResponse.json({ error: "Mindestens ein Vorstandskonto muss erhalten bleiben." }, { status: 400 }); } } const approvalPermissions = normalizeApprovalPermissions(parsed.data.role, parsed.data.approvalPermissions, null); const approvalPreference = getLegacyApprovalPreference(approvalPermissions); const previousSnapshot = snapshotUser(user); const updatedUser = await prisma.user.update({ where: { id }, data: { role: parsed.data.role, workingGroupId, approvalPreference, approvalPermissions: { set: approvalPermissions } } }); const refreshedUser = await prisma.user.findUniqueOrThrow({ where: { id: updatedUser.id }, include: { workingGroup: { select: { name: true } }, _count: { select: { approvals: true, createdExpenses: true } } } }); await createAuditLog(prisma, { actorId: viewer.id, action: "user.update", entityType: "user", entityId: updatedUser.id, entityLabel: updatedUser.username, summary: `Nutzer ${updatedUser.username} wurde aktualisiert.`, metadata: { workingGroupId: updatedUser.workingGroupId, role: updatedUser.role, approvalPermissions: updatedUser.approvalPermissions, rollback: { kind: "user.update", previous: previousSnapshot } } }); return NextResponse.json({ user: serializeManagedUser(refreshedUser) }); } export async function DELETE(_: Request, { params }: Context) { const { id } = await params; const viewer = await getCurrentViewer(); if (!viewer) { return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 }); } if (!canManageUsers(viewer.role)) { return NextResponse.json({ error: "Nur Vorstand oder Finanz-AG dürfen Nutzer löschen." }, { status: 403 }); } if (viewer.id === id) { return NextResponse.json({ error: "Du kannst dein eigenes Konto hier nicht löschen." }, { status: 400 }); } const user = await prisma.user.findUnique({ where: { id }, include: { _count: { select: { approvals: true, createdExpenses: true } } } }); if (!user) { return NextResponse.json({ error: "Nutzer nicht gefunden." }, { status: 404 }); } if (user._count.approvals > 0 || user._count.createdExpenses > 0) { return NextResponse.json( { error: "Nutzer mit Freigaben oder Ausgaben können nicht gelöscht werden." }, { status: 400 } ); } if (user.role === "ADMIN") { const adminCount = await prisma.user.count({ where: { role: "ADMIN" } }); if (adminCount <= 1) { return NextResponse.json({ error: "Mindestens ein Admin muss erhalten bleiben." }, { status: 400 }); } } await prisma.user.delete({ where: { id } }); await createAuditLog(prisma, { actorId: viewer.id, action: "user.delete", entityType: "user", entityId: user.id, entityLabel: user.username, summary: `Nutzer ${user.username} wurde geloescht.`, metadata: { rollback: { kind: "user.delete", deleted: snapshotUser(user) } } }); return NextResponse.json({ ok: true }); }