# ============================================== # 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 .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 )