Files
einkaufsapp/backend/app/runtime_settings.py

184 lines
6.1 KiB
Python

"""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
]