slop game oder so
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user