# ============================================== # Breakpilot Drive - Unit API Helpers # ============================================== # Auth, database, token, and validation helpers for the Unit API. # Extracted from unit_api.py for file-size compliance. from fastapi import HTTPException, Request from typing import Optional, Dict, Any, List from datetime import datetime, timedelta import os import logging import jwt from unit_models import ValidationError, ValidationResult logger = logging.getLogger(__name__) # Feature flags USE_DATABASE = os.getenv("GAME_USE_DATABASE", "true").lower() == "true" REQUIRE_AUTH = os.getenv("GAME_REQUIRE_AUTH", "false").lower() == "true" SECRET_KEY = os.getenv("JWT_SECRET_KEY", "dev-secret-key-change-in-production") # ============================================== # Auth Dependency (reuse from game_api) # ============================================== async def get_optional_current_user(request: Request) -> Optional[Dict[str, Any]]: """Optional auth dependency for Unit API.""" if not REQUIRE_AUTH: return None try: from auth import get_current_user return await get_current_user(request) except ImportError: logger.warning("Auth module not available") return None except HTTPException: raise except Exception as e: logger.error(f"Auth error: {e}") raise HTTPException(status_code=401, detail="Authentication failed") # ============================================== # Database Integration # ============================================== _unit_db = None async def get_unit_database(): """Get unit database instance with lazy initialization.""" global _unit_db if not USE_DATABASE: return None if _unit_db is None: try: from unit.database import get_unit_db _unit_db = await get_unit_db() logger.info("Unit database initialized") except ImportError: logger.warning("Unit database module not available") except Exception as e: logger.warning(f"Unit database not available: {e}") return _unit_db # ============================================== # Token Helpers # ============================================== def create_session_token(session_id: str, student_id: str, expires_hours: int = 4) -> str: """Create a JWT session token for telemetry authentication.""" payload = { "session_id": session_id, "student_id": student_id, "exp": datetime.utcnow() + timedelta(hours=expires_hours), "iat": datetime.utcnow(), } return jwt.encode(payload, SECRET_KEY, algorithm="HS256") def verify_session_token(token: str) -> Optional[Dict[str, Any]]: """Verify a session token and return payload.""" try: return jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) except jwt.ExpiredSignatureError: return None except jwt.InvalidTokenError: return None async def get_session_from_token(request: Request) -> Optional[Dict[str, Any]]: """Extract and verify session from Authorization header.""" auth_header = request.headers.get("Authorization", "") if not auth_header.startswith("Bearer "): return None token = auth_header[7:] return verify_session_token(token) # ============================================== # Validation # ============================================== def validate_unit_definition(unit_data: Dict[str, Any]) -> ValidationResult: """ Validate a unit definition structure. Returns validation result with errors and warnings. """ errors: List[ValidationError] = [] warnings: List[ValidationError] = [] # Required fields if not unit_data.get("unit_id"): errors.append(ValidationError(field="unit_id", message="unit_id ist erforderlich")) if not unit_data.get("template"): errors.append(ValidationError(field="template", message="template ist erforderlich")) elif unit_data["template"] not in ["flight_path", "station_loop"]: errors.append(ValidationError( field="template", message="template muss 'flight_path' oder 'station_loop' sein" )) # Validate stops stops = unit_data.get("stops", []) if not stops: errors.append(ValidationError(field="stops", message="Mindestens 1 Stop erforderlich")) else: # Check minimum stops for flight_path if unit_data.get("template") == "flight_path" and len(stops) < 3: warnings.append(ValidationError( field="stops", message="FlightPath sollte mindestens 3 Stops haben", severity="warning" )) # Validate each stop stop_ids = set() for i, stop in enumerate(stops): if not stop.get("stop_id"): errors.append(ValidationError( field=f"stops[{i}].stop_id", message=f"Stop {i}: stop_id fehlt" )) else: if stop["stop_id"] in stop_ids: errors.append(ValidationError( field=f"stops[{i}].stop_id", message=f"Stop {i}: Doppelte stop_id '{stop['stop_id']}'" )) stop_ids.add(stop["stop_id"]) # Check interaction type interaction = stop.get("interaction", {}) if not interaction.get("type"): errors.append(ValidationError( field=f"stops[{i}].interaction.type", message=f"Stop {stop.get('stop_id', i)}: Interaktionstyp fehlt" )) elif interaction["type"] not in [ "aim_and_pass", "slider_adjust", "slider_equivalence", "sequence_arrange", "toggle_switch", "drag_match", "error_find", "transfer_apply" ]: warnings.append(ValidationError( field=f"stops[{i}].interaction.type", message=f"Stop {stop.get('stop_id', i)}: Unbekannter Interaktionstyp '{interaction['type']}'", severity="warning" )) # Check for label if not stop.get("label"): warnings.append(ValidationError( field=f"stops[{i}].label", message=f"Stop {stop.get('stop_id', i)}: Label fehlt", severity="warning" )) # Validate duration duration = unit_data.get("duration_minutes", 0) if duration < 3 or duration > 20: warnings.append(ValidationError( field="duration_minutes", message="Dauer sollte zwischen 3 und 20 Minuten liegen", severity="warning" )) # Validate difficulty if unit_data.get("difficulty") and unit_data["difficulty"] not in ["base", "advanced"]: warnings.append(ValidationError( field="difficulty", message="difficulty sollte 'base' oder 'advanced' sein", severity="warning" )) return ValidationResult( valid=len(errors) == 0, errors=errors, warnings=warnings )