Löschung/Datenexport (DSGVO) und Retention folgen in Phase 2.
+ {/* Kern-Einstellung: Schutzbedarf / TISAX-Tiefe (nur Superadmin) */}
+
+
Schutzbedarf / TISAX-Tiefe
+
Steuert die Schutzbedarf-Flags des Richtlinienmoduls. Aktuell: {tenant.settings?.tisaxLevel ?? "AL2"}.
+
+
+
+
+
+
{/* Nutzer-Kurzinfo */}
Benutzer
diff --git a/src/components/platform-tenant-users.tsx b/src/components/platform-tenant-users.tsx
index 005b74b..2d8f0cf 100644
--- a/src/components/platform-tenant-users.tsx
+++ b/src/components/platform-tenant-users.tsx
@@ -2,8 +2,8 @@
import { useActionState } from "react";
import {
- createTenantUser, resetTenantUserPassword, setTenantUserStatus, setTenantUserRoles,
- type CreateUserState, type ResetPwState,
+ createTenantUser, updateTenantUser, resetTenantUserPassword, setTenantUserStatus, setTenantUserRoles,
+ type CreateUserState, type ResetPwState, type EditUserState,
} from "@/server/actions/platform-users";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -15,6 +15,7 @@ export interface ManagedUser { id: string; name: string; email: string; status:
const createInit: CreateUserState = { status: "idle" };
const resetInit: ResetPwState = { status: "idle" };
+const editInit: EditUserState = { status: "idle" };
function OneTimeSecret({ label, value }: { label: string; value: string }) {
return (
@@ -88,15 +89,31 @@ function ResetPasswordButton({ tenantId, userId }: { tenantId: string; userId: s
);
}
+function EditUserForm({ tenantId, user }: { tenantId: string; user: ManagedUser }) {
+ const [state, action, pending] = useActionState(updateTenantUser.bind(null, tenantId, user.id), editInit);
+ return (
+
+ );
+}
+
function UserRow({ tenantId, user, roles }: { tenantId: string; user: ManagedUser; roles: RoleOption[] }) {
const active = user.status === "ACTIVE";
return (
-
-
{user.name}
-
{user.email}
-
+
{active ? "Aktiv" : "Deaktiviert"}
diff --git a/src/components/tenant-users-manager.tsx b/src/components/tenant-users-manager.tsx
index a29e9fb..8de41cb 100644
--- a/src/components/tenant-users-manager.tsx
+++ b/src/components/tenant-users-manager.tsx
@@ -2,8 +2,8 @@
import { useActionState } from "react";
import {
- createUser, resetUserPassword, setUserStatus, setUserRoles,
- type CreateUserState, type ResetPwState,
+ createUser, updateUser, resetUserPassword, setUserStatus, setUserRoles,
+ type CreateUserState, type ResetPwState, type EditUserState,
} from "@/server/actions/tenant-users";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -15,6 +15,7 @@ export interface ManagedUser { id: string; name: string; email: string; status:
const createInit: CreateUserState = { status: "idle" };
const resetInit: ResetPwState = { status: "idle" };
+const editInit: EditUserState = { status: "idle" };
function OneTimeSecret({ label, value }: { label: string; value: string }) {
return (
@@ -86,15 +87,31 @@ function ResetPasswordButton({ userId }: { userId: string }) {
);
}
+function EditUserForm({ user }: { user: ManagedUser }) {
+ const [state, action, pending] = useActionState(updateUser.bind(null, user.id), editInit);
+ return (
+
+ );
+}
+
function UserRow({ user, roles }: { user: ManagedUser; roles: RoleOption[] }) {
const active = user.status === "ACTIVE";
return (
-
-
{user.name} {user.isSelf && (Sie)}
-
{user.email}
-
+
{active ? "Aktiv" : "Deaktiviert"}
diff --git a/src/server/actions/admin.ts b/src/server/actions/admin.ts
index 58f5ebe..dfb0946 100644
--- a/src/server/actions/admin.ts
+++ b/src/server/actions/admin.ts
@@ -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({
diff --git a/src/server/actions/platform-users.ts b/src/server/actions/platform-users.ts
index d778718..f60516c 100644
--- a/src/server/actions/platform-users.ts
+++ b/src/server/actions/platform-users.ts
@@ -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
{
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 {
+ 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 {
diff --git a/src/server/actions/tenant-settings.ts b/src/server/actions/tenant-settings.ts
index d625e06..c1f0441 100644
--- a/src/server/actions/tenant-settings.ts
+++ b/src/server/actions/tenant-settings.ts
@@ -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");
}
diff --git a/src/server/actions/tenant-users.ts b/src/server/actions/tenant-users.ts
index 4443581..e862d09 100644
--- a/src/server/actions/tenant-users.ts
+++ b/src/server/actions/tenant-users.ts
@@ -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 {
+ 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 {
const { session, tenantId, db } = await ctx("user:manage");