"use server"; import { revalidatePath } from "next/cache"; import { AuditTargetType, UserRole } from "@prisma/client"; import { z } from "zod"; import { requireRole } from "@/lib/auth-utils"; import { prisma } from "@/lib/prisma"; const deleteKitaSchema = z.object({ kitaId: z.string().min(1), confirmationName: z.string().min(1).trim(), }); const deleteUserSchema = z.object({ userId: z.string().min(1), confirmationEmail: z.string().email().toLowerCase().trim(), }); const userIdSchema = z.string().min(1); type ActionResult = { ok: boolean; error?: string; }; async function requireSuperadminActor() { const session = await requireRole([UserRole.SUPERADMIN]); return { id: session.user.id, email: session.user.email ?? session.user.name ?? "Unbekannter Superadmin", }; } async function getKitaDeleteCounts(kitaId: string) { const [ users, families, children, termine, absences, announcements, invitations, eventParticipations, ] = await Promise.all([ prisma.user.count({ where: { kitaId } }), prisma.family.count({ where: { kitaId } }), prisma.child.count({ where: { kitaId } }), prisma.termin.count({ where: { kitaId } }), prisma.absence.count({ where: { kitaId } }), prisma.announcement.count({ where: { kitaId } }), prisma.invitation.count({ where: { kitaId } }), prisma.eventParticipation.count({ where: { kitaId } }), ]); return { users, families, children, termine, absences, announcements, invitations, eventParticipations, }; } export async function deleteKita( rawPayload: unknown, ): Promise { const actor = await requireSuperadminActor(); const parsed = deleteKitaSchema.safeParse(rawPayload); if (!parsed.success) { return { ok: false, error: "Ungültige Eingabedaten." }; } const kita = await prisma.kita.findUnique({ where: { id: parsed.data.kitaId }, select: { id: true, name: true, slug: true }, }); if (!kita) { return { ok: false, error: "Kita wurde nicht gefunden." }; } if (parsed.data.confirmationName !== kita.name) { return { ok: false, error: "Die Bestätigung muss exakt dem Kita-Namen entsprechen.", }; } const counts = await getKitaDeleteCounts(kita.id); await prisma.$transaction(async (tx) => { await tx.auditLog.create({ data: { actorUserId: actor.id, actorEmail: actor.email, action: "KITA_DELETE", targetType: AuditTargetType.KITA, targetId: kita.id, targetLabel: `${kita.name} (${kita.slug})`, targetKitaId: kita.id, metadata: counts, }, }); await tx.kita.delete({ where: { id: kita.id } }); }); revalidatePath("/admin"); revalidatePath("/admin/audit"); return { ok: true, redirectTo: "/admin" }; } async function findMutableTenantUser(userId: string, actorId: string) { const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true, firstName: true, lastName: true, role: true, kitaId: true, familyId: true, failedLoginAttempts: true, lockedUntil: true, kita: { select: { name: true, slug: true, }, }, family: { select: { name: true, _count: { select: { users: true, }, }, }, }, }, }); if (!user) { return { error: "Benutzer wurde nicht gefunden." } as const; } if (user.id === actorId) { return { error: "Das eigene Superadmin-Konto kann hier nicht geändert werden." } as const; } if (user.role === UserRole.SUPERADMIN) { return { error: "Andere Superadmins werden in V1 nicht verwaltet." } as const; } if (!user.kitaId) { return { error: "Nur tenant-gebundene Benutzer können verwaltet werden." } as const; } return { user } as const; } export async function deleteUser(rawPayload: unknown): Promise { const actor = await requireSuperadminActor(); const parsed = deleteUserSchema.safeParse(rawPayload); if (!parsed.success) { return { ok: false, error: "Ungültige Eingabedaten." }; } const lookup = await findMutableTenantUser(parsed.data.userId, actor.id); if ("error" in lookup) { return { ok: false, error: lookup.error }; } const { user } = lookup; if (parsed.data.confirmationEmail !== user.email) { return { ok: false, error: "Die Bestätigung muss exakt der Benutzer-E-Mail entsprechen.", }; } await prisma.$transaction(async (tx) => { await tx.verificationToken.deleteMany({ where: { identifier: user.id }, }); await tx.auditLog.create({ data: { actorUserId: actor.id, actorEmail: actor.email, action: "USER_DELETE", targetType: AuditTargetType.USER, targetId: user.id, targetLabel: `${user.firstName} ${user.lastName} <${user.email}>`, targetKitaId: user.kitaId, metadata: { role: user.role, kitaName: user.kita?.name ?? null, kitaSlug: user.kita?.slug ?? null, familyId: user.familyId, familyName: user.family?.name ?? null, familyUserCountBeforeDelete: user.family?._count.users ?? null, }, }, }); await tx.user.delete({ where: { id: user.id } }); }); revalidatePath("/admin"); revalidatePath(`/admin/kitas/${user.kitaId}`); revalidatePath("/admin/audit"); return { ok: true }; } async function updateUserSupportState( userId: string, action: "USER_LOCK" | "USER_UNLOCK" | "USER_FAILED_LOGIN_RESET", data: { lockedUntil?: Date | null; failedLoginAttempts?: number; }, ): Promise { const actor = await requireSuperadminActor(); const parsed = userIdSchema.safeParse(userId); if (!parsed.success) { return { ok: false, error: "Ungültige Eingabedaten." }; } const lookup = await findMutableTenantUser(parsed.data, actor.id); if ("error" in lookup) { return { ok: false, error: lookup.error }; } const { user } = lookup; await prisma.$transaction(async (tx) => { await tx.user.update({ where: { id: user.id }, data, }); await tx.auditLog.create({ data: { actorUserId: actor.id, actorEmail: actor.email, action, targetType: AuditTargetType.USER, targetId: user.id, targetLabel: `${user.firstName} ${user.lastName} <${user.email}>`, targetKitaId: user.kitaId, metadata: { role: user.role, previousLockedUntil: user.lockedUntil?.toISOString() ?? null, previousFailedLoginAttempts: user.failedLoginAttempts, }, }, }); }); revalidatePath("/admin"); revalidatePath(`/admin/kitas/${user.kitaId}`); revalidatePath("/admin/audit"); return { ok: true }; } export async function lockUser(userId: string): Promise { return updateUserSupportState(userId, "USER_LOCK", { lockedUntil: new Date("2099-12-31T23:59:59.000Z"), }); } export async function unlockUser(userId: string): Promise { return updateUserSupportState(userId, "USER_UNLOCK", { lockedUntil: null, failedLoginAttempts: 0, }); } export async function resetFailedLoginAttempts( userId: string, ): Promise { return updateUserSupportState(userId, "USER_FAILED_LOGIN_RESET", { failedLoginAttempts: 0, }); }