feat(dsgvo): Löschkonzept/Aufbewahrungsfristen (Auto-Purge)
Security Audit / Python Dependency Audit (push) Has been cancelled
Security Audit / Node.js Dependency Audit (push) Has been cancelled

Neuer retention_service.py: Lohn-/zeitrelevante Daten (time_entries,
hours_payouts) werden nach konfigurierbarer Frist gelöscht
(company.settings.retention_lohn_years, Default 10 Jahre). Technische
Tabellen mit fester Frist: audit_logs (3 Jahre), abgelaufene
sessions/password_resets (sofort).

Täglicher Scheduler-Job (03:00 Uhr, Redis-Tageslock analog Reminder-Jobs)
plus manuelle Trigger: POST /companies/me/run-retention-purge
(COMPANY_ADMIN/HR, nur eigene Firma) und POST /admin/run-retention-purge
(SUPER_ADMIN, global inkl. technischer Tabellen).

Letzter offener Punkt aus dem DSGVO-Löschkonzept (Art. 15/17 bereits erledigt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gis16MnuwkYcivLrSxK1pD
This commit is contained in:
2026-08-27 09:04:27 +02:00
co-authored by Claude Sonnet 5
parent 71e3e82a4e
commit c60c0d6c90
6 changed files with 271 additions and 1 deletions
+19
View File
@@ -23,6 +23,7 @@ from app.models.user import User, UserRole
from app.models.vacation_balance import VacationBalance
from app.services.absence_service import absence_service
from app.services.email_service import email_service
from app.services.retention_service import run_retention_purge
log = logging.getLogger(__name__)
@@ -188,6 +189,22 @@ async def _run_job(name: str, fn) -> None:
log.exception("Reminder-Job %s fehlgeschlagen: %s", name, exc)
async def _run_purge_job(name: str, fn) -> None:
if not _acquire_daily_lock(name):
log.info("Purge-Job %s bereits von anderem Prozess übernommen.", name)
return
from sqlalchemy import text
from app.core.database import AsyncSessionLocal
try:
async with AsyncSessionLocal() as db:
await db.execute(text("SET LOCAL app.bypass_rls = 'on'"))
result = await fn(db)
await db.commit()
log.info("Purge-Job %s: %s", name, result)
except Exception as exc:
log.exception("Purge-Job %s fehlgeschlagen: %s", name, exc)
def start() -> None:
global _scheduler
if _scheduler is not None:
@@ -207,6 +224,8 @@ def start() -> None:
args=["carryover_expiry", run_carryover_expiry], id="carryover_expiry")
_scheduler.add_job(_run_job, CronTrigger(hour=hour, minute=20),
args=["certificate_overdue", run_certificate_overdue], id="certificate_overdue")
_scheduler.add_job(_run_purge_job, CronTrigger(hour=3, minute=0),
args=["retention_purge", run_retention_purge], id="retention_purge")
_scheduler.start()
log.info("Reminder-Scheduler gestartet (täglich ab %02d:00 Uhr).", hour)