feat(PROJ-32): Message-ID-basierte Duplikatserkennung

- message_id Spalte + UNIQUE-Index in emails-Tabelle
- Save() prüft Message-ID vor SHA-256-Flow (kein Disk-I/O bei Duplikat)
- lookupByMessageID() als private Hilfsfunktion
- insertMeta() schreibt message_id, gibt error zurück (Race-safe)
- SaveMeta() schreibt message_id idempotent (Backfill)

feat(PROJ-34): Retention-Policy + Löschsperre (GoBD)

- retain_until TIMESTAMPTZ Spalte in emails-Tabelle
- ErrRetentionLock typed error
- Delete() prüft Retention-Frist vor Löschung
- Purge() löscht alle Mails mit abgelaufener Retention
- POST /api/admin/purge Endpunkt (superadmin only)
- config: storage.retention_days

fix: Superadmin-Benutzerübersicht zeigt Mandant-Spalte

- UsersTab: Mandant-Spalte wenn isSuperAdmin
- domain_auditor Rolle im Create-Dialog ergänzt
- storage Modulversion → 1.6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-31 01:29:25 +02:00
parent cb31c48ce8
commit b6856af2eb
9 changed files with 200 additions and 30 deletions
+1
View File
@@ -840,6 +840,7 @@ export default function AdminPage() {
<UsersTab
isSuperAdmin={isSuperAdmin}
users={users}
tenants={tenants}
usersLoading={usersLoading}
usersError={usersError}
dialogOpen={dialogOpen}
+12
View File
@@ -1,6 +1,7 @@
"use client";
import { type User } from "@/lib/api";
import { type Tenant } from "@/lib/api/tenants";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -35,6 +36,7 @@ import {
interface UsersTabProps {
isSuperAdmin: boolean;
users: User[];
tenants?: Tenant[];
usersLoading: boolean;
usersError: string;
// Create dialog
@@ -61,6 +63,7 @@ interface UsersTabProps {
export function UsersTab({
isSuperAdmin,
users,
tenants = [],
usersLoading,
usersError,
dialogOpen,
@@ -138,6 +141,7 @@ export function UsersTab({
<SelectContent>
<SelectItem value="user">User</SelectItem>
<SelectItem value="auditor">Auditor</SelectItem>
<SelectItem value="domain_auditor">Domain Auditor</SelectItem>
<SelectItem value="domain_admin">Domain Admin</SelectItem>
{isSuperAdmin && <SelectItem value="superadmin">Superadmin</SelectItem>}
</SelectContent>
@@ -186,6 +190,7 @@ export function UsersTab({
<TableHead>Benutzername</TableHead>
<TableHead>E-Mail</TableHead>
<TableHead>Rolle</TableHead>
{isSuperAdmin && <TableHead>Mandant</TableHead>}
<TableHead>Status</TableHead>
<TableHead className="text-right">Aktionen</TableHead>
</TableRow>
@@ -198,6 +203,13 @@ export function UsersTab({
<TableCell>
<Badge variant="secondary">{u.role}</Badge>
</TableCell>
{isSuperAdmin && (
<TableCell className="text-muted-foreground text-sm">
{u.tenant_id
? (tenants.find((t) => t.id === u.tenant_id)?.name ?? `#${u.tenant_id}`)
: <span className="italic"></span>}
</TableCell>
)}
<TableCell>
<Badge variant={u.active ? "default" : "destructive"}>
{u.active ? "Aktiv" : "Inaktiv"}