"""Glory Defense – Flask-App-Factory. Die App ist bewusst monolithisch und zustandslos (Zustand nur in SQLite), damit sie unter Phusion Passenger mit mehreren Prozessen und ohne Hintergrunddienste läuft. """ from __future__ import annotations from collections.abc import Mapping from typing import Any from flask import Flask, Response, jsonify, request from werkzeug.exceptions import HTTPException from werkzeug.middleware.proxy_fix import ProxyFix from . import api, models, routes from .config import INSTANCE_DIR, load_config #: Content-Security-Policy: Skripte nur vom eigenen Server (Phaser liegt lokal). #: ``blob:``/``data:`` braucht Phaser, weil es geladene Bilder, SVGs und Audio als #: Blob-URLs dekodiert und eingebaute Standardtexturen als Data-URLs mitbringt. CONTENT_SECURITY_POLICY = "; ".join( [ "default-src 'self'", "script-src 'self'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob:", "media-src 'self' data: blob:", "font-src 'self' data:", "connect-src 'self' data: blob:", "worker-src 'self' blob:", "object-src 'none'", "base-uri 'self'", "form-action 'self'", "frame-ancestors 'self'", ] ) def _apply_security_headers(response: Response) -> Response: response.headers.setdefault("Content-Security-Policy", CONTENT_SECURITY_POLICY) response.headers.setdefault("X-Content-Type-Options", "nosniff") response.headers.setdefault("Referrer-Policy", "same-origin") response.headers.setdefault("X-Frame-Options", "SAMEORIGIN") return response def _handle_http_error(exc: HTTPException) -> Response | HTTPException: """API-Pfade bekommen Fehler als JSON, damit das Frontend sie einheitlich auswerten kann.""" if request.path.startswith("/api/"): body: dict[str, Any] = { "error": (exc.name or "error").lower().replace(" ", "_"), "message": exc.description or "", } response = jsonify(body) response.status_code = exc.code or 500 return response return exc def create_app(test_config: Mapping[str, Any] | None = None) -> Flask: """Erzeugt und konfiguriert die Flask-App. Args: test_config: Optionale Konfigurationswerte mit Vorrang vor den Umgebungsvariablen (vor allem für Tests). Returns: Die fertig konfigurierte Flask-Anwendung. """ app = Flask(__name__, instance_path=str(INSTANCE_DIR)) app.config.from_mapping(load_config(test_config)) # Antworten in der Reihenfolge, in der der Code die Felder aufbaut (lesbarer). app.json.sort_keys = False # type: ignore[attr-defined] if app.config["TRUST_PROXY"]: # Nur hinter einem vertrauenswürdigen Proxy aktivieren, sonst könnten Clients # ihre IP (und damit das Rate-Limit) per Header fälschen. Übernommen werden nur # Client-IP und Protokoll: Host und Pfadpräfix aus Headern landeten sonst in den # erzeugten URLs (z. B. X-Forwarded-Prefix: //fremde.domain). app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1) # type: ignore[method-assign] models.init_app(app) app.register_blueprint(routes.bp) app.register_blueprint(api.bp) app.after_request(_apply_security_headers) app.register_error_handler(HTTPException, _handle_http_error) return app