"use client"; import { useActionState, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import type { BackupActionState } from "@/server/actions/backup-admin"; /** * Client-Formulare des Betreiber-Portals für Datensicherung/DSGVO. Rendern in * den -Popups der Mandantenseite (?restore=1 / ?dsgvo=1). Die Server- * Actions kommen als (gebundene) Props; die Sicherheitskontrollen (Voll-Admin, * MFA-Step-up, getippte Bestätigung) werden serverseitig erzwungen — die UI * spiegelt sie nur wider. */ type Action = (prev: BackupActionState, formData: FormData) => Promise; export interface SnapshotOption { snapshotId: string; snapshotAt: string | null; totalRows: number | null; artifactTenantSlug: string | null; tenantMismatch: boolean; tables: { model: string; rows: number }[]; } function Feedback({ state }: { state: BackupActionState }) { if (state.status === "error") { return

{state.message}

; } if (state.status === "done") { return

{state.message}

; } return null; } /** Portal-Restore: Auswahl + Dry-run-Vorschau + TOTP + getippte Bestätigung. */ export function RestoreModalBody({ tenantId, tenantSlug, snapshots, action, mfaEnrolled, storeError = false, }: { tenantId: string; tenantSlug: string; snapshots: SnapshotOption[]; action: Action; mfaEnrolled: boolean; /** true = Sicherungsspeicher (S3/MinIO) nicht erreichbar → anderer Hinweis als „keine Sicherung". */ storeError?: boolean; }) { const [state, formAction, pending] = useActionState(action, { status: "idle" }); const [selected, setSelected] = useState(snapshots[0]?.snapshotId ?? ""); const preview = snapshots.find((s) => s.snapshotId === selected) ?? null; const expected = `RESTORE ${tenantSlug}`; return (
Destruktiv: Der Mandant wird gesperrt, sein Datenbestand ersetzt (Wipe + Reinsert). Ein Pre-Restore-Sicherheitsschnappschuss wird automatisch erstellt (reversibel). Ausführung im Worker.
{storeError ? (

Sicherungsspeicher nicht erreichbar (S3/MinIO nicht konfiguriert oder Bucket fehlt). Bitte die S3_*-Variablen und das Backup-Bucket prüfen.

) : snapshots.length === 0 ? (

Kein Sicherungspunkt vorhanden. Zuerst „Export jetzt“ ausführen.

) : ( <>
{snapshots.map((s) => ( ))}
{/* Read-only Dry-run/Vorschau VOR der Bestätigung */} {preview && (

Vorschau (Dry-run)

Zielmandant: {tenantSlug} · Artefakt-Mandant: {preview.artifactTenantSlug ?? "?"} {preview.tenantMismatch && (Mismatch — würde abgewiesen)}

Snapshot: {preview.snapshotAt ? new Date(preview.snapshotAt).toLocaleString() : "—"} · {preview.totalRows ?? "?"} Zeilen gesamt

{preview.tables.length > 0 && (

{preview.tables.slice(0, 8).map((t) => `${t.model}: ${t.rows}`).join(" · ")} {preview.tables.length > 8 ? " …" : ""}

)}
)} {mfaEnrolled && (
)}
)} ); } /** „Export jetzt" — On-demand-Sicherung. */ export function ExportModalBody({ tenantId, action, mfaEnrolled }: { tenantId: string; action: Action; mfaEnrolled: boolean }) { const [state, formAction, pending] = useActionState(action, { status: "idle" }); return (

Erstellt einen neuen, verschlüsselten Sicherungspunkt im Objektspeicher (Worker) — für Restore und regelmäßige Server-Backups.

{mfaEnrolled && (
)}

Oder die Sicherung direkt auf deinen Rechner herunterladen — als verschlüsselte .cvb-Datei. Läuft inline (kein Worker/S3 nötig).

); } export interface SubjectOption { identityId: string; label: string } /** DSGVO-Zustellung: Per-Mandant oder Per-Person, Ergebnis als signierter Link. */ export function DsgvoModalBody({ tenantId, action, subjects, mfaEnrolled, }: { tenantId: string; action: Action; subjects: SubjectOption[]; mfaEnrolled: boolean; }) { const [state, formAction, pending] = useActionState(action, { status: "idle" }); const [scope, setScope] = useState<"tenant" | "person">("tenant"); return (
{scope === "person" && (
)} {mfaEnrolled && (
)}

Der Download-Link ist zeitlich begrenzt und nur im Betreiber-Portal (angemeldet) abrufbar.

); }