[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>
This commit is contained in:
204
backend-lehrer/unit_helpers.py
Normal file
204
backend-lehrer/unit_helpers.py
Normal file
@@ -0,0 +1,204 @@
|
||||
# ==============================================
|
||||
# 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
|
||||
)
|
||||
Reference in New Issue
Block a user