Weisse Seite beheben: Swagger lokal ausliefern, Startseite ergänzen

Zwei Fehler, beide erst im Browser sichtbar:

1. /docs blieb weiss. FastAPI holt Swagger von cdn.jsdelivr.net, und die
   in Phase 3 eingeführte CSP (script-src 'self') blockiert das zu Recht.
   Die Dateien liegen jetzt unter app/static/swagger/ im Repository - das
   funktioniert auch ohne Internet und ruft keinen fremden Server auf, aus
   demselben Grund, aus dem die Kantone-App ihre Schriften lokal ausliefert.
   Das Favicon holte FastAPI ebenfalls von aussen; auch das ist jetzt lokal.

2. Die Wurzel / lieferte ein nacktes 404-JSON - im Browser sieht das aus
   wie eine kaputte Anwendung. Jetzt eine schlichte Platzhalter-Seite, bis
   die Galerie in Phase 4 steht.

Ausserdem beim Testen aufgefallen: eine lokale .env-Datei reicht
ADMIN_PASSWORD auch dann herein, wenn die Umgebungsvariable gelöscht wird -
bei pydantic-settings haben Umgebungsvariablen Vorrang, ein leerer Wert
überstimmt die Datei, ein Löschen nicht. Der Test setzt jetzt leer statt zu
löschen; im Betrieb ist das dieselbe Falle.

Der neue Test prüft, dass /docs keinen einzigen Verweis auf einen fremden
Host enthält - genau das würde sonst unbemerkt wieder hereinrutschen.

61 Tests, alle grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
This commit is contained in:
2026-08-29 23:17:42 +02:00
co-authored by Claude Opus 5
parent 99ba74330c
commit 01cd0a73e8
5 changed files with 105 additions and 5 deletions
+59 -3
View File
@@ -8,9 +8,12 @@ Plan.md).
import logging
import secrets
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from fastapi.staticfiles import StaticFiles
from slowapi.errors import RateLimitExceeded
from starlette.middleware.sessions import SessionMiddleware
@@ -47,6 +50,12 @@ app = FastAPI(
description="Zu klein gewordene Kinderkleidung katalogisieren und weitergeben.",
version="0.1.0",
lifespan=lebenszyklus,
# Eigene /docs-Route weiter unten: die mitgelieferte lädt Swagger von
# cdn.jsdelivr.net, was die CSP (script-src 'self') zu Recht blockiert -
# die Seite bliebe weiss. Ausserdem ginge bei jedem Aufruf die IP des
# Besuchers an einen Dritten.
docs_url=None,
redoc_url=None,
)
_cfg = einstellungen()
@@ -113,10 +122,57 @@ app.include_router(items.router)
app.include_router(images.router)
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
@app.get("/docs", include_in_schema=False)
def swagger_ui():
"""Swagger-Oberfläche mit lokal ausgelieferten Dateien.
Die Dateien liegen unter app/static/swagger/ im Repository - so
funktioniert die Seite auch ohne Internetzugang, und es wird kein
fremder Server aufgerufen.
"""
return get_swagger_ui_html(
openapi_url=app.openapi_url,
title=f"{app.title} - API",
swagger_js_url="/static/swagger/swagger-ui-bundle.js",
swagger_css_url="/static/swagger/swagger-ui.css",
# Ohne das holt FastAPI das Favicon von fastapi.tiangolo.com -
# wieder ein fremder Server, wieder von der CSP blockiert.
swagger_favicon_url="/static/favicon.svg",
)
@app.get("/", include_in_schema=False)
def startseite():
"""Platzhalter, bis die Galerie steht (Phase 4).
Ohne diese Route liefert die Wurzel ein nacktes 404-JSON - im Browser
sieht das aus wie eine kaputte Anwendung.
"""
return HTMLResponse(
"""<!doctype html>
<html lang="de-CH"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>Kinderkleider-Börse</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 34rem; margin: 4rem auto;
padding: 0 1rem; line-height: 1.6; color: #1f2937; }
code { background: #f3f4f6; padding: .1rem .35rem; border-radius: .25rem; }
</style></head><body>
<h1>Kinderkleider-Börse</h1>
<p>Die Anwendung läuft. Die Oberfläche entsteht noch bis dahin lässt sich
alles über die <a href="/docs">API-Oberfläche</a> ausprobieren.</p>
<p>Zum Erfassen zuerst <code>POST /api/v1/auth/login</code> ausführen.
Ohne Anmeldung sind Galerie und Reservierung nutzbar.</p>
</body></html>"""
)
@app.get("/robots.txt", include_in_schema=False)
def robots():
from fastapi.responses import PlainTextResponse
return PlainTextResponse("User-agent: *\nDisallow: /\n")
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#0f766e"/>
<path d="M11 8h10l3 4-3 2v10H11V14l-3-2z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 181 B

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long