Files
breakpilot-lehrer/backend-lehrer/unit_helpers.py
Benjamin Admin 6811264756 [split-required] Split final batch of monoliths >1000 LOC
Python (6 files in klausur-service):
- rbac.py (1,132 → 4), admin_api.py (1,012 → 4)
- routes/eh.py (1,111 → 4), ocr_pipeline_geometry.py (1,105 → 5)

Python (2 files in backend-lehrer):
- unit_api.py (1,226 → 6), game_api.py (1,129 → 5)

Website (6 page files):
- 4x klausur-korrektur pages (1,249-1,328 LOC each) → shared components
  in website/components/klausur-korrektur/ (17 shared files)
- companion (1,057 → 10), magic-help (1,017 → 8)

All re-export barrels preserve backward compatibility.
Zero import errors verified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 23:17:30 +02:00

205 lines
7.1 KiB
Python

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