Files
p42.f-w/backend/app/main.py
T
guyg1474andClaude Opus 5.5 bb20135876 Datenschutz: Bild-Metadaten, private Vorschläge, fremde Bilder, API-Doku
- Uploads: Server entfernt EXIF/XMP/Text (GPS-Standort, Kamera, Uhrzeit)
  per Pillow; Drehung von Hochkant-Fotos wird vorher angewendet, Farbprofil
  bleibt; GIFs unverändert (neue Abhängigkeit pillow)
- Produktvorschläge pro Liste (Tabelle list_products): Einträge privater
  Listen tauchen bei anderen weder in Vorschlägen noch beim Rezeptabgleich
  auf; "Vorschläge verwalten" löscht nur aus eigenen sichtbaren Listen
- Artikel: Bilder nur noch aus den eigenen Uploads (kein Tracking-Pixel
  fremder Server); Bereinigung in htmlclean.py
- /api/docs und /api/openapi.json nur noch im Entwicklungsmodus
  (RELOAD=1 oder P42_API_DOCS=1)
- Migration Schritt 2: bestehende Vorschläge den Listen zuordnen, Artikel
  neu bereinigen, gespeicherte Bilder mit Metadaten säubern
- Tests in test_datenschutz.py (u. a. Rezept-Zuordnung, Umstellung)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:48:19 +02:00

98 lines
3.4 KiB
Python
Raw 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.
"""P42 Dashboard – FastAPI-Backend.
Liefert die API unter /api, die hochgeladenen Bilder unter /uploads
und (falls gebaut) das React-Frontend aus frontend/dist.
"""
import asyncio
import os
import re
from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from . import auth, haushalt, kanban, links, monitoring, projects, reisen, rezept
from .auth import current_user
from .common import ValidationError
from .config import FRONTEND_DIST
from .tracking import track
@asynccontextmanager
async def lifespan(_: FastAPI):
# Verlauf der Server-Auslastung (P42-S.M.) im Hintergrund sammeln
sampler = asyncio.create_task(monitoring.sample_loop())
# Erledigte Kanban-Aufgaben nach 14 Tagen löschen
cleaner = asyncio.create_task(kanban.cleanup_loop())
# Hauptstadt-Kennzeichen älterer Reise-Städte nachtragen (für den Karten-Export)
capitals = asyncio.create_task(reisen.capitals_loop())
yield
sampler.cancel()
cleaner.cancel()
capitals.cancel()
# API-Beschreibung nur bei der Entwicklung (RELOAD=1 oder P42_API_DOCS=1) – öffentlich verriete sie alle Schnittstellen
API_DOCS = os.environ.get("RELOAD") == "1" or os.environ.get("P42_API_DOCS") == "1"
app = FastAPI(
title="P42 Dashboard",
docs_url="/api/docs" if API_DOCS else None,
openapi_url="/api/openapi.json" if API_DOCS else None,
lifespan=lifespan,
)
# Komprimiert u. a. die JS-Bundles und die Weltkarte (~740 KB → ~200 KB)
app.add_middleware(GZipMiddleware, minimum_size=1000)
# ---------- Fehler einheitlich als {"error": "..."} ----------
@app.exception_handler(HTTPException)
async def http_error(_: Request, exc: HTTPException):
return JSONResponse({"error": exc.detail}, status_code=exc.status_code)
@app.exception_handler(ValidationError)
async def validation_error(_: Request, exc: ValidationError):
return JSONResponse({"error": str(exc)}, status_code=400)
# ---------- API ----------
app.include_router(auth.router)
app.include_router(links.router)
app.include_router(projects.router)
app.include_router(monitoring.router)
app.include_router(haushalt.router)
app.include_router(rezept.router)
app.include_router(reisen.router)
app.include_router(kanban.router)
@app.get("/api/track")
async def get_tracking(number: str = "", _: dict = Depends(current_user)):
number = re.sub(r"\s+", "", number).upper()
if not re.fullmatch(r"[A-Z0-9-]{6,40}", number):
raise HTTPException(400, "Ungültige Sendungsnummer.")
return await track(number)
@app.api_route("/api/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"], include_in_schema=False)
def api_not_found(path: str):
raise HTTPException(404, "Nicht gefunden.")
# ---------- React-Frontend (nach `npm run build`) ----------
if (FRONTEND_DIST / "index.html").exists():
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
@app.get("/{path:path}", include_in_schema=False)
def spa(path: str):
file = (FRONTEND_DIST / path).resolve()
if path and file.is_file() and file.is_relative_to(FRONTEND_DIST.resolve()):
return FileResponse(file)
# Alle anderen Pfade übernimmt der React-Router
return FileResponse(FRONTEND_DIST / "index.html", headers={"Cache-Control": "no-cache"})