167 lines
5.7 KiB
Python
167 lines
5.7 KiB
Python
"""App-Fabrik: baut die FastAPI-Anwendung aus Kern und Plugins zusammen.
|
|
|
|
Ablauf bei create_app():
|
|
1. Plugins entdecken (Entry-Points + plugins/-Verzeichnis)
|
|
2. Jinja-Umgebung aufbauen (Kern-Templates + Plugin-Templates)
|
|
3. Engine/Session erstellen, Kern- und Plugin-Migrationen ausführen
|
|
4. Plugin-Lifecycle on_load() aufrufen, Routen registrieren
|
|
5. Initialen Admin anlegen, falls die Benutzertabelle leer ist
|
|
|
|
Beim Herunterfahren (Lifespan) werden on_unload() und die Engine freigegeben.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import RedirectResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.templating import Jinja2Templates
|
|
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, select_autoescape
|
|
from sqlalchemy import func, select
|
|
from starlette.middleware.sessions import SessionMiddleware
|
|
|
|
from redaktionskern.auth import routen as auth_routen
|
|
from redaktionskern.auth.deps import AccessDenied, NotAuthenticated
|
|
from redaktionskern.auth.models import ROLLEN_ANZEIGE, Role, User
|
|
from redaktionskern.auth.security import hash_password
|
|
from redaktionskern.config import Settings
|
|
from redaktionskern.contracts import Migration, PluginContext
|
|
from redaktionskern.db import make_engine, make_session_factory
|
|
from redaktionskern.migrationen import run_migrations
|
|
from redaktionskern.plugin_loader import discover_plugins
|
|
|
|
CORE_TEMPLATE_DIR = Path(__file__).resolve().parent / "templates"
|
|
|
|
|
|
def _kern_migrationen() -> list[Migration]:
|
|
def benutzer_tabelle(conn) -> None:
|
|
User.__table__.create(conn, checkfirst=True)
|
|
|
|
return [Migration(version="0001_benutzer", up=benutzer_tabelle)]
|
|
|
|
|
|
def _initialen_admin_anlegen(session_factory, settings: Settings) -> None:
|
|
with session_factory() as db:
|
|
vorhanden = db.scalar(select(func.count()).select_from(User))
|
|
if vorhanden:
|
|
return
|
|
db.add(
|
|
User(
|
|
username="admin",
|
|
display_name="Administration",
|
|
password_hash=hash_password(settings.initial_admin_password),
|
|
role=Role.ADMIN.value,
|
|
active=True,
|
|
)
|
|
)
|
|
db.commit()
|
|
|
|
|
|
def create_app(settings: Settings | None = None) -> FastAPI:
|
|
settings = settings or Settings()
|
|
|
|
# 1) Plugins entdecken
|
|
registry = discover_plugins(settings.plugins_dir, settings.entry_point_group)
|
|
plugins = registry.all()
|
|
|
|
# 2) Jinja-Umgebung: Kern zuerst, dann Plugin-Templates
|
|
template_dirs = [CORE_TEMPLATE_DIR]
|
|
template_dirs += [d for d in (p.templates_dir() for p in plugins) if d]
|
|
env = Environment(
|
|
loader=ChoiceLoader([FileSystemLoader(str(d)) for d in template_dirs]),
|
|
autoescape=select_autoescape(default=True, default_for_string=True),
|
|
)
|
|
templates = Jinja2Templates(env=env)
|
|
|
|
# 3) Datenbank + Migrationen
|
|
engine = make_engine(settings.database_url)
|
|
session_factory = make_session_factory(engine)
|
|
context = PluginContext(
|
|
engine=engine,
|
|
session_factory=session_factory,
|
|
settings=settings,
|
|
templates=templates,
|
|
registry=registry,
|
|
)
|
|
|
|
with engine.begin() as conn:
|
|
run_migrations(conn, "core", _kern_migrationen())
|
|
for plugin in plugins:
|
|
with engine.begin() as conn:
|
|
run_migrations(conn, plugin.name, plugin.migrations())
|
|
|
|
# 4) Lifecycle + Router
|
|
for plugin in plugins:
|
|
plugin.on_load(context)
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_: FastAPI):
|
|
yield
|
|
for plugin in reversed(plugins):
|
|
plugin.on_unload()
|
|
engine.dispose()
|
|
|
|
app = FastAPI(
|
|
title="Spiele-Redaktion",
|
|
lifespan=lifespan,
|
|
docs_url=None,
|
|
redoc_url=None,
|
|
openapi_url=None,
|
|
)
|
|
# Statische Kern-Dateien (z. B. „Powered by BGG“-Logo, Pflicht für
|
|
# öffentliche BGG-XML-API-Anwendungen).
|
|
statisch = Path(__file__).parent / "static"
|
|
app.mount("/static", StaticFiles(directory=statisch), name="statisch")
|
|
app.state.settings = settings
|
|
app.state.engine = engine
|
|
app.state.session_factory = session_factory
|
|
app.state.templates = templates
|
|
app.state.registry = registry
|
|
|
|
templates.env.globals["app_name"] = "Spiele-Redaktion"
|
|
templates.env.globals["rollen_anzeige"] = ROLLEN_ANZEIGE
|
|
templates.env.globals["plugin_nav"] = [
|
|
{"label": eintrag.label, "url": eintrag.url}
|
|
for plugin in plugins
|
|
for eintrag in plugin.navigation()
|
|
]
|
|
templates.env.globals["geladene_plugins"] = plugins
|
|
|
|
app.include_router(auth_routen.router)
|
|
app.include_router(auth_routen.admin_router)
|
|
for plugin in plugins:
|
|
app.include_router(plugin.router)
|
|
|
|
app.add_middleware(
|
|
SessionMiddleware,
|
|
secret_key=settings.session_secret,
|
|
max_age=settings.session_dauer_sekunden,
|
|
same_site="lax",
|
|
)
|
|
|
|
def _aktueller_benutzer_aus_session(request: Request) -> User | None:
|
|
user_id = request.session.get("user_id")
|
|
if user_id is None:
|
|
return None
|
|
with session_factory() as db:
|
|
return db.get(User, user_id)
|
|
|
|
@app.exception_handler(NotAuthenticated)
|
|
async def nicht_angemeldet_handler(_: Request, __: NotAuthenticated):
|
|
return RedirectResponse("/login", status_code=303)
|
|
|
|
@app.exception_handler(AccessDenied)
|
|
async def kein_zugriff_handler(request: Request, _: AccessDenied):
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="403.html",
|
|
context={"user": _aktueller_benutzer_aus_session(request)},
|
|
status_code=403,
|
|
)
|
|
|
|
# 5) Initialer Admin
|
|
_initialen_admin_anlegen(session_factory, settings)
|
|
return app
|