Files
2026-09-24 17:57:54 +02:00

90 lines
3.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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