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:
+59
-3
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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
+37
-2
@@ -40,8 +40,15 @@ def test_falsches_passwort(gast):
|
||||
|
||||
|
||||
def test_ohne_passwort_ist_der_bereich_gesperrt(gast, monkeypatch, kategorie_id):
|
||||
"""Ohne hinterlegtes Passwort bleibt gesperrt statt offen zu stehen."""
|
||||
monkeypatch.delenv("ADMIN_PASSWORD", raising=False)
|
||||
"""Ohne hinterlegtes Passwort bleibt gesperrt statt offen zu stehen.
|
||||
|
||||
Leerer Wert statt delenv: Umgebungsvariablen haben bei pydantic-settings
|
||||
Vorrang vor der .env-Datei. Ein blosses Löschen brächte hier gar nichts,
|
||||
weil eine lokale .env den Wert sonst wieder hereinreicht - dieselbe
|
||||
Falle, die auch im Betrieb zuschlagen kann.
|
||||
"""
|
||||
monkeypatch.setenv("ADMIN_PASSWORD", "")
|
||||
monkeypatch.setenv("ADMIN_PASSWORD_HASH", "")
|
||||
einstellungen.cache_clear()
|
||||
security.hash_zwischenspeicher_leeren()
|
||||
|
||||
@@ -168,3 +175,31 @@ def test_robots_txt_sperrt_suchmaschinen(gast):
|
||||
antwort = gast.get("/robots.txt")
|
||||
assert antwort.status_code == 200
|
||||
assert "Disallow: /" in antwort.text
|
||||
|
||||
|
||||
def test_startseite_antwortet(gast):
|
||||
"""Ohne eigene Route lieferte die Wurzel ein nacktes 404-JSON - im
|
||||
Browser sieht das aus wie eine kaputte Anwendung."""
|
||||
antwort = gast.get("/")
|
||||
assert antwort.status_code == 200
|
||||
assert "Kinderkleider-Börse" in antwort.text
|
||||
|
||||
|
||||
def test_swagger_laedt_nichts_von_fremden_servern(gast):
|
||||
"""Die mitgelieferte /docs-Seite holt Swagger von cdn.jsdelivr.net.
|
||||
Die CSP (script-src 'self') blockiert das zu Recht - die Seite bliebe
|
||||
weiss. Ausserdem ginge dabei die IP jedes Besuchers an einen Dritten.
|
||||
"""
|
||||
import re
|
||||
|
||||
antwort = gast.get("/docs")
|
||||
assert antwort.status_code == 200
|
||||
|
||||
# Kein einziger Verweis auf einen anderen Host
|
||||
fremde = re.findall(r'(?:src|href)="(https?:)?//[^"]+"', antwort.text)
|
||||
assert not fremde, f"lädt von aussen: {fremde}"
|
||||
assert "/static/swagger/swagger-ui-bundle.js" in antwort.text
|
||||
|
||||
# Und die Dateien sind auch wirklich da
|
||||
assert gast.get("/static/swagger/swagger-ui-bundle.js").status_code == 200
|
||||
assert gast.get("/static/swagger/swagger-ui.css").status_code == 200
|
||||
|
||||
Reference in New Issue
Block a user