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