diff --git a/src/auth.py b/src/auth.py index f60e259..7d5d80b 100644 --- a/src/auth.py +++ b/src/auth.py @@ -59,6 +59,41 @@ def decode_token(token: str) -> dict: ) +# --- Aktivitaets-Erfassung fuer MAU/DAU --------------------------------------- +# Je Nutzer und Kalendertag genau ein Eintrag in user_activity_days. Der +# In-Memory-Merker haelt die DB-Last bei einem INSERT je Nutzer und Tag. +# Ein Fehler hier darf NIEMALS eine Anfrage blockieren. +_activity_seen: set = set() + + +async def _track_activity(user_id: int, tenant_id) -> None: + day = datetime.now(TIMEZONE).strftime("%Y-%m-%d") + key = (user_id, day) + if key in _activity_seen: + return + _activity_seen.add(key) + if len(_activity_seen) > 20000: + # Speicher begrenzen, alte Tage interessieren den Merker nicht mehr + _activity_seen.clear() + _activity_seen.add(key) + try: + import aiosqlite + from config import DB_PATH + db = await aiosqlite.connect(DB_PATH) + try: + await db.execute( + "INSERT OR IGNORE INTO user_activity_days (user_id, day, tenant_id) VALUES (?, ?, ?)", + (user_id, day, tenant_id), + ) + await db.commit() + finally: + await db.close() + except Exception: + # Bewusst schlucken (z.B. Tabelle fehlt noch). Der Merker behaelt den + # Schluessel, damit ein Dauerfehler nicht jede Anfrage erneut trifft. + pass + + async def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security), ) -> dict: @@ -69,6 +104,7 @@ async def get_current_user( detail="Nicht authentifiziert", ) payload = decode_token(credentials.credentials) + await _track_activity(int(payload["sub"]), payload.get("tenant_id")) return { "id": int(payload["sub"]), "username": payload["username"], diff --git a/src/database.py b/src/database.py index 1cc0592..b54e145 100644 --- a/src/database.py +++ b/src/database.py @@ -920,6 +920,24 @@ async def init_db(): await db.commit() logger.info("Migration: billing_tariff angelegt und mit Konfigurationswerten befuellt") + # Migration: Aktivitaets-Tage je Nutzer fuer die MAU/DAU-Statistik im + # Verwaltungsportal. Geschrieben von auth.get_current_user (ein Eintrag + # je Nutzer und Tag), gelesen nur vom Portal. + cursor = await db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='user_activity_days'" + ) + if not await cursor.fetchone(): + await db.execute(""" + CREATE TABLE user_activity_days ( + user_id INTEGER NOT NULL, + day TEXT NOT NULL, + tenant_id INTEGER, + PRIMARY KEY (user_id, day) + ) + """) + await db.commit() + logger.info("Migration: user_activity_days angelegt (MAU/DAU-Erfassung)") + # Migration: Token-Usage-Monatstabelle cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='token_usage_monthly'") if not await cursor.fetchone():