import hashlib import secrets from datetime import datetime, timedelta, timezone import bcrypt from jose import JWTError, jwt from .config import get_settings settings = get_settings() # bcrypt akzeptiert maximal 72 Bytes – längere Passwörter werden abgeschnitten. _BCRYPT_MAX_BYTES = 72 def hash_password(password: str) -> str: pw = password.encode("utf-8")[:_BCRYPT_MAX_BYTES] return bcrypt.hashpw(pw, bcrypt.gensalt()).decode("utf-8") def verify_password(password: str, password_hash: str | None) -> bool: # Konten, die es nur ueber Single Sign-on gibt, haben gar keinen Hash. Ohne # diesen Riegel liefe None.encode() in einen AttributeError – und der Login # antwortete mit 500 statt mit einem ehrlichen 401. if not password_hash: return False try: pw = password.encode("utf-8")[:_BCRYPT_MAX_BYTES] return bcrypt.checkpw(pw, password_hash.encode("utf-8")) except (ValueError, TypeError): return False def create_access_token( user_id: int, role: str, gueltig_minuten: int | None = None ) -> str: """Stellt ein Sitzungs-Token aus, das den Benutzer ueber seine ID benennt. Frueher stand hier der Benutzername. Damit war der Name die Kennung der Sitzung – und ein Umbenennen haette jede laufende Anmeldung dieses Benutzers ungueltig gemacht. Ueber die ID ist der Name reine Anzeige und frei aenderbar. ``uid`` steht zusaetzlich zu ``sub`` drin, damit sich alte Token (``sub`` = Name) eindeutig von neuen unterscheiden lassen – ein Benutzer koennte sonst schlicht "42" heissen. ``gueltig_minuten`` kommt aus der Einstellung ``session_days``; ``None`` nimmt den Rueckfallwert aus der Umgebung. **0 heisst unbegrenzt** – dann bekommt das Token gar kein ``exp`` und laeuft nie ab. Diese Wahl ist bewusst folgenreich: Ein ausgestelltes Token laesst sich nicht zurueckrufen, es gibt keine Sperrliste. Wer alle Anmeldungen beenden will, muss JWT_SECRET aendern. """ minuten = settings.jwt_expire_minutes if gueltig_minuten is None else gueltig_minuten payload: dict = {"sub": str(user_id), "uid": user_id, "role": role} if minuten > 0: payload["exp"] = datetime.now(timezone.utc) + timedelta(minutes=minuten) return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) def generate_api_token() -> str: """Erzeugt ein langlebiges Token für externe Zugriffe.""" return secrets.token_urlsafe(32) def hash_api_token(token: str) -> str: """SHA-256 – schnell genug, um bei jeder Anfrage geprüft zu werden.""" return hashlib.sha256(token.encode("utf-8")).hexdigest() def decode_access_token(token: str) -> dict | None: try: return jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]) except JWTError: return None