Dateien
AegisSight-Monitor/src/auth.py
claude-dev d367c60b26 feat(statistik): Aktivitäts-Tage je Nutzer erfassen (MAU/DAU-Grundlage)
Neue Tabelle user_activity_days (user_id, day, tenant_id, ein Eintrag je
Nutzer und Kalendertag), angelegt per idempotenter Startup-Migration.
Geschrieben wird sie in get_current_user bei jeder authentifizierten
Nutzung, ein In-Memory-Tagesmerker begrenzt das auf einen INSERT OR
IGNORE je Nutzer und Tag, Fehler werden geschluckt und blockieren nie
eine Anfrage. Gelesen wird die Tabelle nur vom Verwaltungsportal für
die neue MAU/DAU-Statistik. Ein reines Login-Protokoll wäre zu grob,
weil das JWT 24 Stunden hält.

Getestet gegen eine DB-Kopie, Tabelle entsteht, je Nutzer und Tag
genau ein Eintrag trotz Mehrfachaufruf.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:01:00 +00:00

123 Zeilen
3.8 KiB
Python

"""JWT-Authentifizierung mit Magic-Link-Support und Multi-Tenancy."""
import secrets
from datetime import datetime, timedelta
from jose import jwt, JWTError
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from config import get_jwt_secret, JWT_ALGORITHM, JWT_EXPIRE_HOURS, TIMEZONE
security = HTTPBearer(auto_error=False)
JWT_ISSUER = "intelsight-osint"
JWT_AUDIENCE = "intelsight-osint"
def create_token(
user_id: int,
username: str,
email: str,
role: str = "member",
tenant_id: int = None,
org_slug: str = None,
is_global_admin: bool = False,
) -> str:
"""JWT-Token erstellen mit Tenant-Kontext."""
now = datetime.now(TIMEZONE)
expire = now + timedelta(hours=JWT_EXPIRE_HOURS)
payload = {
"sub": str(user_id),
"username": username,
"email": email,
"role": role,
"tenant_id": tenant_id,
"org_slug": org_slug,
"is_global_admin": is_global_admin,
"iss": JWT_ISSUER,
"aud": JWT_AUDIENCE,
"iat": now,
"exp": expire,
}
return jwt.encode(payload, get_jwt_secret(), algorithm=JWT_ALGORITHM)
def decode_token(token: str) -> dict:
"""JWT-Token dekodieren und validieren."""
try:
payload = jwt.decode(
token,
get_jwt_secret(),
algorithms=[JWT_ALGORITHM],
issuer=JWT_ISSUER,
audience=JWT_AUDIENCE,
)
return payload
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token ungueltig oder abgelaufen",
)
# --- 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:
"""FastAPI Dependency: Aktuellen Nutzer aus Token extrahieren."""
if credentials is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
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"],
"email": payload.get("email", ""),
"role": payload.get("role", "member"),
"tenant_id": payload.get("tenant_id"),
"org_slug": payload.get("org_slug"),
"is_global_admin": payload.get("is_global_admin", False),
}
def generate_magic_token() -> str:
"""Generiert einen 64-Zeichen URL-safe Token."""
return secrets.token_urlsafe(48)