Big Sweep: Security + Race-Conditions + Tests + DSGVO + A11y, SW by-v1095
SECURITY (auth.py, routes/auth.py, database.py, main.py) - JWT bekommt jti; Logout trägt in neue jwt_blacklist-Tabelle ein, decode_token() prüft → server-side Invalidierung - JWT-Expiry default 30 → 7 Tage (ENV JWT_EXPIRY_DAYS überschreibt) - Sliding-Refresh-Middleware: erneuert Cookie wenn >50% verbraucht (Schwelle via JWT_REFRESH_FRACTION, Default 2) - Login-Lockout in DB-Tabelle login_attempts (5 Versuche / 15 Min, überlebt Container-Restart) — alte In-Memory-Lockouts ersetzt - SMTP-Versand: alle 'except: pass' durch logger.exception ersetzt; Fehlversuche landen in failed_emails-Tabelle für späteres Retry - Referral-Counter Race gefixt: UPDATE partner_codes SET uses=uses+1 ... WHERE uses<max_uses RETURNING — atomar statt SELECT+UPDATE RACE CONDITIONS (routes/invoices.py, database.py) - Neue invoice_counters-Tabelle für atomare Nummernvergabe - _next_invoice_number nutzt BEGIN IMMEDIATE + atomares UPDATE - Funktioniert für RG- und ST-Prefixe (Stornorechnungen) - Race-Test verifiziert (5 Threads × 20 Calls = 100 eindeutige Nummern) VERSION + TESTS + ERROR-DIGEST (VERSION, Makefile, tests/, scheduler.py) - Neue VERSION-Datei (Single Source of Truth) — main.py liest beim Startup - Makefile-Target 'make bump' propagiert in sw.js, app.js, index.html - Makefile-Target 'make test' setzt venv auf, läuft pytest - 19 Smoke-Tests in tests/ (health, auth, diary, invoice) — alle grün - Scheduler: täglicher _job_error_digest um 06:30 → schickt Error- Zusammenfassung an ADMIN_EMAIL (still wenn keine Errors) DSGVO + A11Y + ERSTE-HILFE - landing.html: 'HTML und ODS' → 'JSON' (tatsächlich implementiert) - datenschutz.js: Sektion Account-Löschung erweitert (sofort gelöscht / anonymisiert / 10 Jahre für Rechnungen) - erste-hilfe.js: prominentes Warning-Banner oben (ersetzt keine Tierarzt-Beratung); Notfallnummern gruppiert nach Land, TODO-Platz- halter für AT-Uni-Klinik, CH Tox Info Suisse, CH Tierspital Zürich - ui.js Modal: ESC schließt, Focus-Trap, Auto-Focus erstes Element, Restore Focus auf vorigen Caller - impressum.js Kontaktformular: Labels mit for=cf-name etc. NEUE DB-TABELLEN (idempotent via CREATE TABLE IF NOT EXISTS) - jwt_blacklist, login_attempts, failed_emails, invoice_counters NEUE ENV-VARS - JWT_REFRESH_FRACTION (Default 2) - JWT_EXPIRY_DAYS Default geändert (30 → 7)
This commit is contained in:
parent
6224044654
commit
9394bab1fb
23 changed files with 1208 additions and 78 deletions
|
|
@ -592,10 +592,15 @@ def _migrate(conn_factory):
|
|||
]
|
||||
with conn_factory() as conn:
|
||||
for table, column, col_type in migrations:
|
||||
# PRAGMA table_info() liefert leere Liste fuer nicht-existierende Tabellen.
|
||||
# Wir ueberspringen — die CREATE TABLE-Bloecke darunter legen sie an,
|
||||
# ihre Spalten sind dort dann sowieso schon enthalten.
|
||||
existing = [
|
||||
row[1] for row in
|
||||
conn.execute(f"PRAGMA table_info({table})").fetchall()
|
||||
]
|
||||
if not existing:
|
||||
continue
|
||||
if column not in existing:
|
||||
conn.execute(
|
||||
f"ALTER TABLE {table} ADD COLUMN {column} {col_type}"
|
||||
|
|
@ -1209,7 +1214,7 @@ def _migrate(conn_factory):
|
|||
|
||||
# ki_daily_calls: source-Spalte + PK auf (user_id, date, source) erweitern
|
||||
ki_cols = {r[1] for r in conn.execute("PRAGMA table_info(ki_daily_calls)").fetchall()}
|
||||
if "source" not in ki_cols:
|
||||
if ki_cols and "source" not in ki_cols:
|
||||
conn.executescript("""
|
||||
ALTER TABLE ki_daily_calls RENAME TO ki_daily_calls_old;
|
||||
CREATE TABLE ki_daily_calls (
|
||||
|
|
@ -1301,11 +1306,19 @@ def _migrate(conn_factory):
|
|||
logger.info("Migration: dogs.gewicht_kg aus health synchronisiert.")
|
||||
|
||||
# Performance-Indizes für häufige Queries
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_health_naechstes ON health(naechstes) WHERE naechstes IS NOT NULL")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_ki_calls_user_date ON ki_daily_calls(user_id, date)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_events_user_datum ON events(user_id, datum)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_diary_created ON diary(dog_id, created_at DESC)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(user_id, created_at DESC)")
|
||||
# Defensiv — einige Tabellen werden erst beim ersten Zugriff in
|
||||
# ihren jeweiligen Routes-Modulen angelegt (z.B. ki_daily_calls)
|
||||
for _idx_sql in (
|
||||
"CREATE INDEX IF NOT EXISTS idx_health_naechstes ON health(naechstes) WHERE naechstes IS NOT NULL",
|
||||
"CREATE INDEX IF NOT EXISTS idx_ki_calls_user_date ON ki_daily_calls(user_id, date)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_events_user_datum ON events(user_id, datum)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_diary_created ON diary(dog_id, created_at DESC)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(user_id, created_at DESC)",
|
||||
):
|
||||
try:
|
||||
conn.execute(_idx_sql)
|
||||
except Exception as _e:
|
||||
logger.debug("Index-Migration uebersprungen (%s): %s", _idx_sql.split()[5], _e)
|
||||
logger.info("Migration: Performance-Indizes bereit.")
|
||||
|
||||
# Züchter-Tabellen
|
||||
|
|
@ -2444,7 +2457,17 @@ def _migrate(conn_factory):
|
|||
total REAL NOT NULL
|
||||
)
|
||||
""")
|
||||
logger.info("Migration: invoices + invoice_items bereit.")
|
||||
# Atomare Rechnungsnummern-Vergabe pro (prefix, year)
|
||||
# next_num = naechste zu vergebende Nummer (startet bei 1)
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS invoice_counters (
|
||||
prefix TEXT NOT NULL,
|
||||
year INTEGER NOT NULL,
|
||||
next_num INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (prefix, year)
|
||||
)
|
||||
""")
|
||||
logger.info("Migration: invoices + invoice_items + invoice_counters bereit.")
|
||||
except Exception as e:
|
||||
logger.warning(f"Migration invoices: {e}")
|
||||
|
||||
|
|
@ -2461,6 +2484,63 @@ def _migrate(conn_factory):
|
|||
else:
|
||||
logger.info("Migration: users.geburtstag bereits vorhanden.")
|
||||
|
||||
# ---- Security Hardening (Sprint60) ----
|
||||
# JWT-Blacklist: invalidierte Tokens nach Logout / Sperrungen
|
||||
conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS jwt_blacklist (
|
||||
jti TEXT PRIMARY KEY,
|
||||
expires_at TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_jwt_blacklist_expires ON jwt_blacklist(expires_at);
|
||||
""")
|
||||
logger.info("Migration: jwt_blacklist Tabelle bereit.")
|
||||
|
||||
# Login-Brute-Force-Lockout (persistent statt In-Memory)
|
||||
conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS login_attempts (
|
||||
email TEXT PRIMARY KEY COLLATE NOCASE,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
last_attempt TEXT NOT NULL,
|
||||
locked_until TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_login_attempts_locked ON login_attempts(locked_until);
|
||||
""")
|
||||
logger.info("Migration: login_attempts Tabelle bereit.")
|
||||
|
||||
# Fehlgeschlagene E-Mail-Zustellungen — für Admin-Retry / Diagnose
|
||||
conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS failed_emails (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
to_email TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
error TEXT NOT NULL,
|
||||
context TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_failed_emails_created ON failed_emails(created_at DESC);
|
||||
""")
|
||||
logger.info("Migration: failed_emails Tabelle bereit.")
|
||||
|
||||
# Second-Pass der ALTER-TABLE-Migrations:
|
||||
# Manche Migrations (z.B. diary_media.img_width) referenzieren Tabellen,
|
||||
# die erst weiter unten in dieser Funktion per CREATE IF NOT EXISTS angelegt
|
||||
# werden. Beim ersten Durchgang wurden sie uebersprungen — jetzt nachziehen.
|
||||
for table, column, col_type in migrations:
|
||||
existing = [
|
||||
row[1] for row in
|
||||
conn.execute(f"PRAGMA table_info({table})").fetchall()
|
||||
]
|
||||
if not existing:
|
||||
continue
|
||||
if column not in existing:
|
||||
try:
|
||||
conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {col_type}")
|
||||
logger.info(f"Migration (2nd pass): {table}.{column} hinzugefügt.")
|
||||
except Exception as _e:
|
||||
logger.debug(f"Migration (2nd pass) skipped {table}.{column}: {_e}")
|
||||
|
||||
|
||||
def _seed_help_articles(conn):
|
||||
"""Befüllt help_articles mit Starter-FAQs — nur wenn die Tabelle noch leer ist."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue