109 lines
4.2 KiB
TypeScript
109 lines
4.2 KiB
TypeScript
"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));
|
|
}
|
|
}
|