Benutzer bearbeiten (Name/E-Mail) + Kern-Einstellungen nur Superadmin
Nachschärfung der Benutzer-/Rollenverwaltung auf Basis des Feedbacks. - Nutzer editierbar: Name & E-Mail (eindeutig je Mandant) je Nutzer in beiden Konsolen — updateTenantUser (Plattform) / updateUser (Tenant), inline-Formular in platform-tenant-users.tsx und tenant-users-manager.tsx. Rollen/Status/Reset bestanden bereits (browser-verifiziert: Name/E-Mail, Rollen, Deaktivieren, Reset). - Kern-Einstellungen ausschließlich Superadmin: TISAX-/Schutzbedarf-Tiefe wandert in die Admin-Konsole (setTenantTisaxLevel, AL2/AL3 in /admin/[id]); aus den Tenant-Einstellungen entfernt (nur noch Read-only-Anzeige). updateTenantSettings fasst tisaxLevel/Schutzbedarf-Flags nicht mehr an. Module waren bereits Superadmin. - Tenant-Einstellungen: „Benutzer & Rollen"-Bereich verlinkt und sichtbar (user:manage/role:manage) — Verwaltung unter /settings/users. Browser-verifiziert: Admin-Konsole editiert Name/E-Mail/Rollen/Status/Passwort und schaltet TISAX AL3; /settings zeigt Benutzer-Bereich + TISAX read-only; /settings/users listet Nutzer mit Editierfeldern. tsc + lint + build + Guard grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import { platformAuth } from "@/server/platform-auth";
|
||||
import { prisma } from "@/server/db";
|
||||
import { prisma, dbForTenant } from "@/server/db";
|
||||
import { provisionTenant } from "@/server/provision";
|
||||
|
||||
/** Nur Plattform-Admins über die getrennte Plattform-Session (kein Mandantenkontext). */
|
||||
@@ -57,6 +57,20 @@ export async function setTenantStatus(tenantId: string, status: "ACTIVE" | "SUSP
|
||||
revalidatePath(`/admin/${tenantId}`);
|
||||
}
|
||||
|
||||
/** Kern-Einstellung (nur Superadmin): TISAX-/Schutzbedarf-Tiefe des Mandanten. */
|
||||
export async function setTenantTisaxLevel(tenantId: string, level: "AL2" | "AL3") {
|
||||
const session = await requirePlatformAdmin();
|
||||
const db = dbForTenant(tenantId);
|
||||
await db.tenantSettings.update({ where: { tenantId }, data: { tisaxLevel: level } });
|
||||
// Schutzbedarf-Flags des Richtlinienmoduls: HIGH stets an, VERY_HIGH nur bei AL3.
|
||||
await db.policyVariable.updateMany({ where: { key: "FLAG_HIGH_PROTECTION" }, data: { value: "true" } });
|
||||
await db.policyVariable.updateMany({ where: { key: "FLAG_VERY_HIGH_PROTECTION" }, data: { value: level === "AL3" ? "true" : "false" } });
|
||||
await prisma.auditLog.create({
|
||||
data: { tenantId, scope: "platform", actorId: session.user.id, action: "update", entity: "tisax_level", entityId: tenantId, after: { level } },
|
||||
});
|
||||
revalidatePath(`/admin/${tenantId}`);
|
||||
}
|
||||
|
||||
export async function toggleTenantModule(tenantId: string, moduleKey: string, enabled: boolean) {
|
||||
const session = await requirePlatformAdmin();
|
||||
await prisma.tenantModule.upsert({
|
||||
|
||||
@@ -29,6 +29,11 @@ export type ResetPwState =
|
||||
| { status: "error"; message: string }
|
||||
| { status: "done"; generatedPassword: string };
|
||||
|
||||
export type EditUserState =
|
||||
| { status: "idle" }
|
||||
| { status: "error"; message: string }
|
||||
| { status: "ok" };
|
||||
|
||||
/** Zählt aktive Nutzer mit Mandanten-Admin-Rolle (Lockout-Schutz). */
|
||||
async function countActiveTenantAdmins(db: TenantDb): Promise<number> {
|
||||
return db.user.count({
|
||||
@@ -90,6 +95,27 @@ export async function createTenantUser(tenantId: string, _prev: CreateUserState,
|
||||
return { status: "done", email: email.data, generatedPassword: generated };
|
||||
}
|
||||
|
||||
/** Stammdaten (Name, E-Mail) eines Nutzers ändern; E-Mail bleibt je Mandant eindeutig. */
|
||||
export async function updateTenantUser(tenantId: string, userId: string, _prev: EditUserState, formData: FormData): Promise<EditUserState> {
|
||||
const session = await requirePlatformSession();
|
||||
const db = dbForTenant(tenantId);
|
||||
const user = await db.user.findUnique({ where: { id: userId } });
|
||||
if (!user) return { status: "error", message: "Nutzer nicht gefunden." };
|
||||
|
||||
const name = String(formData.get("name") ?? "").trim();
|
||||
const email = emailSchema.safeParse(String(formData.get("email") ?? "").trim().toLowerCase());
|
||||
if (!name) return { status: "error", message: "Bitte einen Namen angeben." };
|
||||
if (!email.success) return { status: "error", message: "Bitte eine gültige E-Mail-Adresse angeben." };
|
||||
if (email.data !== user.email && (await db.user.findFirst({ where: { email: email.data, id: { not: userId } } }))) {
|
||||
return { status: "error", message: "Diese E-Mail ist in diesem Mandanten bereits vergeben." };
|
||||
}
|
||||
|
||||
await db.user.update({ where: { id: userId }, data: { name, email: email.data } });
|
||||
await writePlatformAudit({ actorId: session.user.id, action: "update", entity: "user", entityId: userId, after: { tenantId, name, email: email.data } });
|
||||
revalidatePath(`/admin/${tenantId}`);
|
||||
return { status: "ok" };
|
||||
}
|
||||
|
||||
/** Passwort zurücksetzen: neues Einmal-Passwort + erzwungener Wechsel. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Signatur von useActionState erzwungen
|
||||
export async function resetTenantUserPassword(tenantId: string, userId: string, _prev: ResetPwState, _formData: FormData): Promise<ResetPwState> {
|
||||
|
||||
@@ -18,8 +18,9 @@ export async function updateTenantSettings(formData: FormData) {
|
||||
const db = dbForTenant(tenantId);
|
||||
|
||||
const orgName = z.string().trim().min(1).parse(formData.get("orgName"));
|
||||
const tisaxLevel = str(formData.get("tisaxLevel")) === "AL3" ? "AL3" : "AL2";
|
||||
|
||||
// TISAX-/Schutzbedarf-Tiefe ist eine Kern-Einstellung und wird ausschließlich vom
|
||||
// Superadmin (Admin-Konsole) gesteuert — hier bewusst nicht anfassen.
|
||||
const data = {
|
||||
orgName,
|
||||
orgShort: str(formData.get("orgShort")) || null,
|
||||
@@ -35,7 +36,6 @@ export async function updateTenantSettings(formData: FormData) {
|
||||
accent: str(formData.get("accent")) || null,
|
||||
locale: str(formData.get("locale")) || "de",
|
||||
timezone: str(formData.get("timezone")) || "Europe/Berlin",
|
||||
tisaxLevel,
|
||||
};
|
||||
|
||||
await db.tenantSettings.upsert({
|
||||
@@ -46,11 +46,8 @@ export async function updateTenantSettings(formData: FormData) {
|
||||
|
||||
// Stammdaten → ISMS-Template-Variablen (eine Pflegestelle)
|
||||
await syncPolicyVariablesFromSettings(prisma, tenantId, data);
|
||||
// TISAX-Level steuert die Schutzbedarf-Flags des Richtlinienmoduls
|
||||
await prisma.policyVariable.updateMany({ where: { tenantId, key: "FLAG_HIGH_PROTECTION" }, data: { value: "true" } });
|
||||
await prisma.policyVariable.updateMany({ where: { tenantId, key: "FLAG_VERY_HIGH_PROTECTION" }, data: { value: tisaxLevel === "AL3" ? "true" : "false" } });
|
||||
|
||||
await writeAuditLog({ tenantId, actorId: session.user.id, action: "update", entity: "tenant_settings", after: { orgName, tisaxLevel } });
|
||||
await writeAuditLog({ tenantId, actorId: session.user.id, action: "update", entity: "tenant_settings", after: { orgName } });
|
||||
revalidatePath("/settings");
|
||||
revalidatePath("/policies");
|
||||
}
|
||||
|
||||
@@ -30,6 +30,11 @@ export type ResetPwState =
|
||||
| { status: "error"; message: string }
|
||||
| { status: "done"; generatedPassword: string };
|
||||
|
||||
export type EditUserState =
|
||||
| { status: "idle" }
|
||||
| { status: "error"; message: string }
|
||||
| { status: "ok" };
|
||||
|
||||
async function ctx(permission: "user:manage" | "role:manage") {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, permission);
|
||||
@@ -88,6 +93,26 @@ export async function createUser(_prev: CreateUserState, formData: FormData): Pr
|
||||
return { status: "done", email: email.data, generatedPassword: generated };
|
||||
}
|
||||
|
||||
/** Stammdaten (Name, E-Mail) eines Nutzers ändern; E-Mail bleibt je Mandant eindeutig. */
|
||||
export async function updateUser(userId: string, _prev: EditUserState, formData: FormData): Promise<EditUserState> {
|
||||
const { session, tenantId, db } = await ctx("user:manage");
|
||||
const user = await db.user.findUnique({ where: { id: userId } });
|
||||
if (!user) return { status: "error", message: "Nutzer nicht gefunden." };
|
||||
|
||||
const name = String(formData.get("name") ?? "").trim();
|
||||
const email = emailSchema.safeParse(String(formData.get("email") ?? "").trim().toLowerCase());
|
||||
if (!name) return { status: "error", message: "Bitte einen Namen angeben." };
|
||||
if (!email.success) return { status: "error", message: "Bitte eine gültige E-Mail-Adresse angeben." };
|
||||
if (email.data !== user.email && (await db.user.findFirst({ where: { email: email.data, id: { not: userId } } }))) {
|
||||
return { status: "error", message: "Diese E-Mail ist bereits vergeben." };
|
||||
}
|
||||
|
||||
await db.user.update({ where: { id: userId }, data: { name, email: email.data } });
|
||||
await writeAuditLog({ tenantId, actorId: session.user.id, action: "update", entity: "user", entityId: userId, after: { name, email: email.data } });
|
||||
revalidatePath("/settings/users");
|
||||
return { status: "ok" };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Signatur von useActionState erzwungen
|
||||
export async function resetUserPassword(userId: string, _prev: ResetPwState, _formData: FormData): Promise<ResetPwState> {
|
||||
const { session, tenantId, db } = await ctx("user:manage");
|
||||
|
||||
Reference in New Issue
Block a user