""" Kiosk-Session-Service: Redis-basierte Short-Lived Sessions für Kiosk-User-Auth. Nach erfolgreicher PIN/QR/NFC-Auth bekommt der Kiosk-Client ein Session-Token (UUID, 15min TTL). Dieses Token wird in allen Stempel-Requests als Header X-Kiosk-Session-Token mitgeschickt. Redis ist Pflicht für Sessions. Bei Redis-Ausfall → 503. """ from __future__ import annotations import json import uuid from datetime import datetime, timezone from typing import Optional from fastapi import HTTPException SESSION_TTL_SECONDS = 15 * 60 # 15 Minuten SESSION_KEY_PREFIX = "kiosk_session:" class KioskSessionService: def _get_redis(self): """Redis-Client aus app.core.redis holen. Raises 503 wenn nicht verfügbar.""" try: from app.core.redis import get_redis_client client = get_redis_client() if client is None: raise HTTPException( status_code=503, detail="Session-Service nicht verfügbar (Redis nicht erreichbar)." ) return client except ImportError: raise HTTPException( status_code=503, detail="Session-Service nicht konfiguriert." ) async def create_session( self, user_id: uuid.UUID, company_id: uuid.UUID, device_id: uuid.UUID, auth_method: str, # "pin" | "nfc" | "qr" | "list" ) -> str: """Erzeugt eine neue Kiosk-Session und speichert sie in Redis.""" redis = self._get_redis() session_token = str(uuid.uuid4()) key = SESSION_KEY_PREFIX + session_token payload = { "user_id": str(user_id), "company_id": str(company_id), "device_id": str(device_id), "auth_method": auth_method, "created_at": datetime.now(timezone.utc).isoformat(), } try: redis.setex(key, SESSION_TTL_SECONDS, json.dumps(payload)) except Exception as exc: raise HTTPException( status_code=503, detail=f"Session konnte nicht erstellt werden: {exc}" ) return session_token async def get_session(self, session_token: str) -> Optional[dict]: """Gibt Session-Daten zurück oder None wenn abgelaufen/nicht vorhanden.""" redis = self._get_redis() key = SESSION_KEY_PREFIX + session_token try: data = redis.get(key) except Exception as exc: raise HTTPException( status_code=503, detail=f"Session-Lookup fehlgeschlagen: {exc}" ) if data is None: return None return json.loads(data) async def invalidate_session(self, session_token: str) -> None: """Session sofort ungültig machen (Logout).""" redis = self._get_redis() key = SESSION_KEY_PREFIX + session_token try: redis.delete(key) except Exception: pass # Best-effort async def refresh_session(self, session_token: str) -> bool: """TTL einer aktiven Session zurücksetzen. Returns False wenn Session nicht mehr existiert.""" redis = self._get_redis() key = SESSION_KEY_PREFIX + session_token try: result = redis.expire(key, SESSION_TTL_SECONDS) return bool(result) except Exception: return False async def require_session(self, session_token: str) -> dict: """Wie get_session, aber wirft 401 wenn Session ungültig/abgelaufen.""" session = await self.get_session(session_token) if session is None: raise HTTPException( status_code=401, detail="Kiosk-Session abgelaufen oder ungültig. Bitte neu anmelden." ) return session kiosk_session_service = KioskSessionService()