fix(PROJ-34): Mandanten-Retention ist Opt-in — kein globaler Lock für Mandanten
storage.Save(): retention_days eines Mandanten muss explizit > 0 sein. Globale config greift nie automatisch auf Mandanten-Mails. Mails ohne Mandant: globale config als Fallback (unveraendert). Frontend: Hinweis und Labels klargestellt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -324,16 +324,19 @@ func (s *Store) Save(ctx context.Context, raw []byte, _ time.Time, tenantID *int
|
||||
} else {
|
||||
s.insertMetaMinimal(ctx, id, len(raw), tenantID)
|
||||
}
|
||||
// PROJ-34: Set retention lock — prefer per-tenant retention, fall back to global.
|
||||
effectiveRetention := s.retentionDays
|
||||
// PROJ-34: Set retention lock.
|
||||
// Mandanten-Mails: nur wenn der Mandant explizit retention_days > 0 gesetzt hat.
|
||||
// Globale config greift NICHT automatisch — jeder Mandant muss selbst opt-in.
|
||||
// Mails ohne Mandant (tenantID == nil): globale config als Fallback.
|
||||
if tenantID != nil {
|
||||
var tenantDays int
|
||||
if err := s.db.QueryRow(ctx, `SELECT retention_days FROM tenants WHERE id=$1`, *tenantID).Scan(&tenantDays); err == nil && tenantDays > 0 {
|
||||
effectiveRetention = tenantDays
|
||||
until := time.Now().AddDate(0, 0, tenantDays)
|
||||
_, _ = s.db.Exec(ctx, `UPDATE emails SET retain_until=$1 WHERE id=$2 AND retain_until IS NULL`, until, id)
|
||||
}
|
||||
}
|
||||
if effectiveRetention > 0 {
|
||||
until := time.Now().AddDate(0, 0, effectiveRetention)
|
||||
// else: tenant hat retention_days=0 → kein Lock gesetzt → keine automatische Löschung
|
||||
} else if s.retentionDays > 0 {
|
||||
until := time.Now().AddDate(0, 0, s.retentionDays)
|
||||
_, _ = s.db.Exec(ctx, `UPDATE emails SET retain_until=$1 WHERE id=$2 AND retain_until IS NULL`, until, id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,17 +63,11 @@ async function triggerPurge(): Promise<number> {
|
||||
return data.deleted ?? 0;
|
||||
}
|
||||
|
||||
function retentionLabel(days: number, globalDays: number): React.ReactNode {
|
||||
function retentionLabel(days: number): React.ReactNode {
|
||||
if (days === 0) {
|
||||
return globalDays > 0 ? (
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Global ({globalDays} Tage)
|
||||
</span>
|
||||
) : (
|
||||
<Badge variant="secondary">Kein Lock</Badge>
|
||||
);
|
||||
return <Badge variant="secondary">Kein Lock (manuell setzen)</Badge>;
|
||||
}
|
||||
return <Badge>{days} Tage</Badge>;
|
||||
return <Badge>{days} Tage ({(days / 365).toFixed(1)} Jahre)</Badge>;
|
||||
}
|
||||
|
||||
export function RetentionTab() {
|
||||
@@ -137,8 +131,6 @@ export function RetentionTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const globalDays = data?.global_retention_days ?? 0;
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
{/* Global Policy */}
|
||||
@@ -149,15 +141,16 @@ export function RetentionTab() {
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium">Globale Aufbewahrungsfrist:</span>
|
||||
{globalDays > 0 ? (
|
||||
<Badge>{globalDays} Tage ({Math.round(globalDays / 365)} Jahre)</Badge>
|
||||
{(data?.global_retention_days ?? 0) > 0 ? (
|
||||
<Badge>{data!.global_retention_days} Tage ({Math.round(data!.global_retention_days / 365)} Jahre)</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">Kein globaler Lock (config.yml)</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Die globale Frist wird in <code className="text-xs bg-muted px-1 rounded">config.yml → storage.retention_days</code> konfiguriert.
|
||||
Mandanten-spezifische Einstellungen überschreiben diese, wenn gesetzt.
|
||||
Die globale Frist gilt nur für Mails ohne Mandant-Zuordnung (Systemkonten).
|
||||
<strong className="text-foreground"> Für Mandanten greift ausschließlich die individuelle Einstellung</strong> —
|
||||
solange ein Mandant keine eigene Frist gesetzt hat (0 Tage), werden seine Mails nie automatisch gelöscht.
|
||||
</p>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -191,7 +184,7 @@ export function RetentionTab() {
|
||||
{(data?.tenants ?? []).map((t) => (
|
||||
<TableRow key={t.id}>
|
||||
<TableCell className="font-medium">{t.name}</TableCell>
|
||||
<TableCell>{retentionLabel(t.retention_days, globalDays)}</TableCell>
|
||||
<TableCell>{retentionLabel(t.retention_days)}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="outline" onClick={() => handleEditOpen(t)}>
|
||||
Bearbeiten
|
||||
|
||||
Reference in New Issue
Block a user