"use server"; import { randomUUID } from "node:crypto"; import { hashSync } from "bcryptjs"; import { redirect, unstable_rethrow } from "next/navigation"; import { requireAdminUser } from "@/lib/auth"; import { db, initDatabase, transaction } from "@/lib/db"; import { idValue, passwordValue, roleValue, usernameValue } from "@/lib/validation"; import type { UserRole } from "@/lib/types"; function go(kind: "error" | "success", message: string): never { redirect(`/admin/users?${kind}=${encodeURIComponent(message)}`); } function errorMessage(error: unknown) { if (error instanceof Error && error.message.includes("UNIQUE constraint failed")) { return "Dieser Benutzername ist bereits vergeben."; } return error instanceof Error ? error.message : "Die Aktion konnte nicht ausgeführt werden."; } export async function createUserAction(formData: FormData) { await requireAdminUser(); initDatabase(); try { const name = usernameValue(formData, "name"); const existing = db .prepare("SELECT 1 FROM users WHERE name = ? COLLATE NOCASE AND active = 1") .get(name); if (existing) throw new Error("Dieser Benutzername ist bereits vergeben."); const password = passwordValue(formData); const role = roleValue(formData); const internalEmail = `user-${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "konto"}-${randomUUID().slice(0, 8)}@local.invalid`; db.prepare( "INSERT INTO users (name, email, password_hash, role) VALUES (?, ?, ?, ?)", ).run(name, internalEmail, hashSync(password, 12), role); go("success", "Benutzer wurde angelegt."); } catch (error) { unstable_rethrow(error); go("error", errorMessage(error)); } } export async function changeUserRoleAction(formData: FormData) { const currentUser = await requireAdminUser(); initDatabase(); try { const id = idValue(formData); const role = roleValue(formData); transaction(() => { const target = db.prepare("SELECT role FROM users WHERE id = ? AND active = 1").get(id) as | { role: UserRole } | undefined; if (!target) throw new Error("Benutzer wurde nicht gefunden."); if (target.role === "admin" && role === "member") { const count = db .prepare("SELECT COUNT(*) AS count FROM users WHERE role = 'admin' AND active = 1") .get() as { count: number }; if (count.count <= 1) throw new Error("Der letzte Administrator kann nicht herabgestuft werden."); if (id === currentUser.id) { throw new Error("Die eigene Administratorrolle kann nicht herabgestuft werden."); } } db.prepare("UPDATE users SET role = ? WHERE id = ? AND active = 1").run(role, id); }); go("success", "Rolle wurde geändert."); } catch (error) { unstable_rethrow(error); go("error", errorMessage(error)); } } export async function deleteUserAction(formData: FormData) { const currentUser = await requireAdminUser(); initDatabase(); try { const id = idValue(formData); if (id === currentUser.id) throw new Error("Das aktuell angemeldete Konto kann nicht gelöscht werden."); transaction(() => { const target = db .prepare("SELECT role FROM users WHERE id = ? AND active = 1") .get(id) as | { role: UserRole } | undefined; if (!target) throw new Error("Benutzer wurde nicht gefunden."); if (target.role === "admin") { const count = db .prepare("SELECT COUNT(*) AS count FROM users WHERE role = 'admin' AND active = 1") .get() as { count: number }; if (count.count <= 1) throw new Error("Der letzte Administrator kann nicht gelöscht werden."); } db.prepare( `UPDATE users SET name = name || ' (gelöscht)', email = ?, password_hash = ?, role = 'member', active = 0, deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND active = 1`, ).run(`deleted-${id}-${randomUUID()}@invalid.local`, hashSync(randomUUID(), 12), id); }); go("success", "Benutzerkonto wurde gelöscht; vorhandene Einträge bleiben erhalten."); } catch (error) { unstable_rethrow(error); go("error", errorMessage(error)); } }