"""Fristen, die sich zur Laufzeit einstellen lassen. Bis hierher standen sie verstreut: teils als Konstante im Quelltext (`INVITE_DAYS = 14`), teils in der Umgebung (`CLEANUP_DELETED_DAYS`), teils in der Einstellungstabelle. Wer nachsehen wollte, was tatsächlich gilt, musste an drei Stellen suchen - und die Datenschutzerklärung behauptete Werte, die niemand überprüfen konnte. Jetzt an einer Stelle, mit klarer Rangfolge: Einstellungstabelle → Umgebungsvariable → Vorgabewert hier Die Umgebungsvariable wirkt also als Startwert; sobald jemand in der Verwaltung etwas ändert, gilt der gespeicherte Wert. Das entspricht dem Verhalten der Selbstregistrierung. Fristen, die NICHT hierhergehören, weil sie außerhalb der Anwendung liegen: die Aufbewahrung der Webserver-Protokolle (Sache der Protokollrotation auf dem Server) und alles, was fremde Dienste mit ihren eigenen Daten tun. """ import logging from dataclasses import dataclass from sqlalchemy.orm import Session from app.config import settings from app.deps import get_setting, set_setting log = logging.getLogger(__name__) @dataclass(frozen=True) class Duration: key: str label: str unit: str # "Tage" | "Monate" | "Stunden" default: int minimum: int maximum: int # Was passiert, wenn 0 eingetragen wird? None = nicht erlaubt. zero_means: str | None = None note: str = "" # Reihenfolge bestimmt die Darstellung in der Verwaltung. DURATIONS: tuple[Duration, ...] = ( # ---- Konten ---- Duration( "auto_deactivate_months", "Konto deaktivieren nach", "Monaten", default=12, minimum=0, maximum=600, zero_means="abgeschaltet", note="ohne Anmeldung; Administratorkonten sind ausgenommen", ), Duration( "auto_delete_months", "Konto löschen nach", "Monaten", default=12, minimum=0, maximum=600, zero_means="abgeschaltet", note="Deaktivierung", ), # ---- Listen ---- Duration( "auto_archive_months", "Liste archivieren nach", "Monaten", default=12, minimum=0, maximum=600, zero_means="abgeschaltet", note="ohne Öffnen; gilt je Person getrennt", ), Duration( "archive_delete_months", "Liste löschen nach", "Monaten", default=12, minimum=0, maximum=600, zero_means="abgeschaltet", note="im Archiv des Eigentümers; Vorwarnung 7 Tage und 1 Tag vorher", ), # ---- Aufbewahrung ---- Duration( "cleanup_deleted_days", "Gelöschtes endgültig entfernen nach", "Tagen", default=30, minimum=1, maximum=365, note="so lange kann ein Gerät offline bleiben und vom Löschen " "noch erfahren", ), Duration( "cleanup_ops_days", "Quittungen der Warteschlange aufbewahren", "Tage", default=7, minimum=1, maximum=90, note="verhindern, dass eine wiederholte Änderung doppelt ausgeführt wird", ), Duration( "product_cache_days", "Produktabfragen gültig", "Tage", default=180, minimum=1, maximum=3650, note="danach wird derselbe Strichcode erneut abgefragt", ), # ---- Sitzungen und Links ---- Duration( "session_days", "Angemeldet bleiben", "Tage", default=30, minimum=1, maximum=365, note="danach ist eine erneute Anmeldung nötig", ), Duration( "invite_days", "Einladung gültig", "Tage", default=14, minimum=1, maximum=90, note="danach muss sie erneut versendet werden", ), Duration( "welcome_days", "Willkommenslink gültig", "Tage", default=14, minimum=1, maximum=90, note="Link zum Festlegen des ersten Passworts", ), Duration( "verify_hours", "Bestätigungslink gültig", "Stunden", default=24, minimum=1, maximum=720, note="Bestätigung der E-Mail-Adresse nach der Registrierung", ), Duration( "reset_hours", "Passwortlink gültig", "Stunden", default=1, minimum=1, maximum=72, note="kurz gehalten: Der Link erlaubt die Übernahme des Kontos", ), Duration( "email_change_hours", "Adresswechsel bestätigen binnen", "Stunden", default=48, minimum=1, maximum=336, note="bei Administratorkonten müssen beide Adressen zustimmen", ), ) BY_KEY = {d.key: d for d in DURATIONS} def _env_default(entry: Duration) -> int: """Startwert aus der Umgebung, falls es dort ein passendes Feld gibt.""" value = getattr(settings, entry.key, None) if isinstance(value, int) and not isinstance(value, bool): return value return entry.default def get_duration(db: Session, key: str) -> int: """Aktuell geltender Wert.""" entry = BY_KEY[key] raw = get_setting(db, key, "") if not raw: return _env_default(entry) try: value = int(raw) except ValueError: log.warning( "Einstellung %s ist keine Zahl (%r) - verwende %d", key, raw, _env_default(entry), ) return _env_default(entry) if entry.zero_means and value == 0: return 0 return max(entry.minimum, min(value, entry.maximum)) def set_duration(db: Session, key: str, value: int) -> int: entry = BY_KEY[key] if entry.zero_means and value == 0: clamped = 0 else: clamped = max(entry.minimum, min(int(value), entry.maximum)) set_setting(db, key, str(clamped)) return clamped def all_durations(db: Session) -> dict[str, int]: return {entry.key: get_duration(db, entry.key) for entry in DURATIONS} def describe_durations(db: Session) -> list[dict]: """Beschreibung für die Verwaltungsoberfläche - Beschriftung, Einheit, Grenzen und Bedeutung der Null kommen aus dieser Datei, damit die Oberfläche sie nicht doppelt vorhalten muss.""" return [ { "key": entry.key, "label": entry.label, "unit": entry.unit, "value": get_duration(db, entry.key), "default": _env_default(entry), "minimum": entry.minimum, "maximum": entry.maximum, "zero_means": entry.zero_means, "note": entry.note, } for entry in DURATIONS ]