diff --git a/app/main.py b/app/main.py index b62a390..1544d20 100644 --- a/app/main.py +++ b/app/main.py @@ -127,6 +127,28 @@ app.mount("/static", StaticFiles(directory=static_dir), name="static") templates = Jinja2Templates(directory=str(templates_dir)) +# ─── Auth-Fehler bei HTML-Seiten: Redirect statt JSON-401/403 ───────────────── + +@app.exception_handler(401) +async def auth_required_redirect(request: Request, exc: HTTPException): + """Bei 401 auf HTML-Routes: Redirect zu /?login=1 (öffnet Auth-Modal).""" + accept = request.headers.get("accept", "") + if "text/html" in accept: + from fastapi.responses import RedirectResponse + return RedirectResponse("/?login=1", status_code=302) + return JSONResponse({"detail": exc.detail}, status_code=401) + + +@app.exception_handler(403) +async def admin_required_redirect(request: Request, exc: HTTPException): + """Bei 403 auf HTML-Routes: Redirect zu /?login=1 (öffnet Auth-Modal).""" + accept = request.headers.get("accept", "") + if "text/html" in accept: + from fastapi.responses import RedirectResponse + return RedirectResponse("/?login=1", status_code=302) + return JSONResponse({"detail": exc.detail}, status_code=403) + + @app.on_event("startup") async def startup(): import asyncio @@ -209,7 +231,7 @@ async def classic_index(request: Request): # ─── Default: / → v2 (Default-Flip #139 Phase 2) ──────────────────────────── @app.get("/", response_class=HTMLResponse) -async def index(request: Request): +async def index(request: Request, current_user: Optional[dict] = Depends(get_current_user)): """Startseite — rendert v2-Listenansicht (Default-Flip Phase 2). Alte /?drucksache=XX-YYYY Deep-Links werden per 301-Redirect auf @@ -230,11 +252,12 @@ async def index(request: Request): "assessments": assessments, "bl_codes": bl_codes, "assessment_count": len(assessments), + **_v2_template_context(current_user), }) @app.get("/antrag/{drucksache:path}", response_class=HTMLResponse) -async def antrag_detail(request: Request, drucksache: str): +async def antrag_detail(request: Request, drucksache: str, current_user: Optional[dict] = Depends(get_current_user)): """v2-Antragsdetail-Route — Server-Side-Rendering mit echten DB-Daten.""" try: drucksache = validate_drucksache(drucksache) @@ -243,6 +266,7 @@ async def antrag_detail(request: Request, drucksache: str): "request": request, "v2_active_nav": "durchsuchen", "error": f"Ungültige Drucksachen-ID: {drucksache}", + **_v2_template_context(current_user), }, status_code=400) row = await get_assessment(drucksache) @@ -251,6 +275,7 @@ async def antrag_detail(request: Request, drucksache: str): "request": request, "v2_active_nav": "durchsuchen", "error": f"Antrag {drucksache} wurde nicht gefunden.", + **_v2_template_context(current_user), }, status_code=404) antrag = _row_to_detail(row) @@ -294,9 +319,33 @@ async def antrag_detail(request: Request, drucksache: str): "E5": "Setzt sich Ihre Kommune für mehr Demokratie ein — auch auf Landes- und Bundesebene? Werden internationale Abkommen unterstützt?", }, "matrix_labels": MATRIX_LABELS, + **_v2_template_context(current_user), }) +def _v2_template_context(current_user=None) -> dict: + """Gemeinsame v2-Template-Variablen: is_admin, is_authenticated, v2_bundeslaender. + + Wird in jeder v2-Route aufgerufen und per **-Spread in den Template-Context gemischt. + """ + is_authenticated = bool(current_user and current_user.get("authenticated", False)) + # require_auth liefert keinen "authenticated"-Key, aber ein sub-Feld — beides prüfen + if current_user and current_user.get("sub"): + is_authenticated = True + roles = (current_user or {}).get("roles", []) + is_admin = "admin" in roles or "gwoe-admin" in roles + v2_bls = [ + {"code": bl.code, "name": bl.name} + for bl in alle_bundeslaender() + if bl.aktiv + ] + return { + "is_authenticated": is_authenticated, + "is_admin": is_admin, + "v2_bundeslaender": v2_bls, + } + + def _rows_to_list(rows): """Konvertiert DB-Rows in das flache Dict-Format für die v2-Listenansicht.""" result = [] @@ -1588,25 +1637,27 @@ async def list_bundeslaender(): # === Impressum / Datenschutz === @app.get("/impressum", response_class=HTMLResponse) -async def impressum_page(request: Request): +async def impressum_page(request: Request, current_user: Optional[dict] = Depends(get_current_user)): return templates.TemplateResponse("v2/screens/legal.html", { "request": request, "app_name": settings.app_name, "title": "Impressum", "section": "impressum", + **_v2_template_context(current_user), }) @app.get("/datenschutz", response_class=HTMLResponse) -async def datenschutz_page(request: Request): +async def datenschutz_page(request: Request, current_user: Optional[dict] = Depends(get_current_user)): return templates.TemplateResponse("v2/screens/legal.html", { "request": request, "app_name": settings.app_name, "title": "Datenschutzerklärung", "section": "datenschutz", + **_v2_template_context(current_user), }) # === Quellen / Programme === @app.get("/methodik", response_class=HTMLResponse) -async def methodik_page(request: Request): +async def methodik_page(request: Request, current_user: Optional[dict] = Depends(get_current_user)): """Transparenz-/Methodik-Seite (#96).""" from .bundeslaender import aktive_bundeslaender, BUNDESLAENDER from .embeddings import get_indexing_status @@ -1630,11 +1681,12 @@ async def methodik_page(request: Request): "programme_count": status.get("total", 0), "chunk_count": sum(p.get("chunks", 0) for p in status.get("programmes", [])), "bundeslaender": sorted(bl_list, key=lambda x: x["name"]), + **_v2_template_context(current_user), }) @app.get("/quellen", response_class=HTMLResponse) -async def quellen_page(request: Request): +async def quellen_page(request: Request, current_user: Optional[dict] = Depends(get_current_user)): """Quellen-Seite mit allen Wahl- und Parteiprogrammen, nach BL gruppiert.""" from .bundeslaender import BUNDESLAENDER programmes = get_programme_info() @@ -1661,6 +1713,7 @@ async def quellen_page(request: Request): "wahlprogramme_grouped": wahlprogramme_grouped, "grundsatzprogramme": grundsatz, "status": status, + **_v2_template_context(current_user), }) @@ -1850,7 +1903,7 @@ async def index_programme( @app.get("/auswertungen", response_class=HTMLResponse) -async def auswertungen_page(request: Request): +async def auswertungen_page(request: Request, current_user: Optional[dict] = Depends(get_current_user)): """Auswertungs-Dashboard in v2 (Phase 3 Migration aus Classic).""" from .auswertungen import get_wahlperioden from .bundeslaender import alle_bundeslaender @@ -1864,6 +1917,7 @@ async def auswertungen_page(request: Request): "v2_active_nav": "auswertungen", "wahlperioden": wahlperioden, "bl_codes": bl_codes, + **_v2_template_context(current_user), }) @@ -2171,26 +2225,28 @@ async def v2_antrag_redirect(request: Request, drucksache: str): @app.get("/v2/merkliste", response_class=HTMLResponse) -async def v2_merkliste(request: Request): - """Merkliste (Bookmarks) — lädt Daten via /api/bookmarks client-seitig.""" +async def v2_merkliste(request: Request, current_user: dict = Depends(require_auth)): + """Merkliste (Bookmarks) — nur für eingeloggte User; lädt Daten via /api/bookmarks client-seitig.""" return templates.TemplateResponse("v2/screens/merkliste.html", { "request": request, "v2_active_nav": "merkliste", + **_v2_template_context(current_user), }) @app.get("/v2/tags", response_class=HTMLResponse) -async def v2_tags(request: Request): +async def v2_tags(request: Request, current_user: Optional[dict] = Depends(get_current_user)): """Tag-Cloud-Seite — Themen-Filter über alle Assessments.""" return templates.TemplateResponse("v2/screens/tags.html", { "request": request, "v2_active_nav": "tags", + **_v2_template_context(current_user), }) @app.get("/v2/cluster", response_class=HTMLResponse) -async def v2_cluster(request: Request): - """Cluster-Liste — Top-10 Cluster als redaktionelle Liste.""" +async def v2_cluster(request: Request, current_user: dict = Depends(require_admin)): + """Cluster-Liste — nur für Admins.""" rows = await get_all_assessments(None) assessments = _rows_to_list(rows) bl_codes = sorted({a["bundesland"] for a in assessments if a.get("bundesland")}) @@ -2198,12 +2254,13 @@ async def v2_cluster(request: Request): "request": request, "v2_active_nav": "cluster", "bl_codes": bl_codes, + **_v2_template_context(current_user), }) @app.get("/v2/neu", response_class=HTMLResponse) -async def v2_neu(request: Request): - """Neuer-Antrag-Form — startet Analyse via /api/analyze-drucksache.""" +async def v2_neu(request: Request, current_user: dict = Depends(require_auth)): + """Neuer-Antrag-Form — nur für eingeloggte User; startet Analyse via /api/analyze-drucksache.""" from .bundeslaender import alle_bundeslaender bl_list = [ {"code": bl.code, "name": bl.name} @@ -2215,12 +2272,13 @@ async def v2_neu(request: Request): "v2_active_nav": "neu", "bundeslaender": sorted(bl_list, key=lambda x: x["name"]), "default_model": settings.llm_model_default, + **_v2_template_context(current_user), }) @app.get("/v2/landtag-suche", response_class=HTMLResponse) -async def v2_landtag_suche(request: Request): - """Landtag-Suche — sucht Drucksachen live im Landtags-Portal (nicht nur DB).""" +async def v2_landtag_suche(request: Request, current_user: dict = Depends(require_auth)): + """Landtag-Suche — nur für eingeloggte User; sucht Drucksachen live im Landtags-Portal.""" from .bundeslaender import alle_bundeslaender bl_list = [ {"code": bl.code, "name": bl.name} @@ -2231,12 +2289,13 @@ async def v2_landtag_suche(request: Request): "request": request, "v2_active_nav": "landtag_suche", "bundeslaender": sorted(bl_list, key=lambda x: x["name"]), + **_v2_template_context(current_user), }) @app.get("/v2/batch", response_class=HTMLResponse) -async def v2_batch(request: Request): - """Batch-Analyse-Form (Admin) — enqueued ungeprüfte Drucksachen eines BL.""" +async def v2_batch(request: Request, current_user: dict = Depends(require_admin)): + """Batch-Analyse-Form — nur für Admins; enqueued ungeprüfte Drucksachen eines BL.""" from .bundeslaender import alle_bundeslaender bl_list = [ {"code": bl.code, "name": bl.name} @@ -2247,6 +2306,7 @@ async def v2_batch(request: Request): "request": request, "v2_active_nav": "batch", "bundeslaender": sorted(bl_list, key=lambda x: x["name"]), + **_v2_template_context(current_user), }) @@ -2258,7 +2318,7 @@ async def v2_admin_freischaltungen(request: Request, user: dict = Depends(requir return templates.TemplateResponse("v2/screens/admin_freischaltungen.html", { "request": request, "v2_active_nav": "admin_freischaltungen", - "is_admin": True, + **_v2_template_context(user), }) @@ -2268,7 +2328,7 @@ async def v2_admin_queue(request: Request, user: dict = Depends(require_admin)): return templates.TemplateResponse("v2/screens/admin_queue.html", { "request": request, "v2_active_nav": "admin_queue", - "is_admin": True, + **_v2_template_context(user), }) @@ -2278,7 +2338,7 @@ async def v2_admin_abos(request: Request, user: dict = Depends(require_admin)): return templates.TemplateResponse("v2/screens/admin_abos.html", { "request": request, "v2_active_nav": "admin_abos", - "is_admin": True, + **_v2_template_context(user), }) diff --git a/app/templates/v2/base.html b/app/templates/v2/base.html index 49aa0a0..3e9dd3a 100644 --- a/app/templates/v2/base.html +++ b/app/templates/v2/base.html @@ -40,26 +40,26 @@ {{ assessment_count }} {% endif %} - {{ icon("bookmark-simple", 14) }} Merkliste - {{ icon("tag", 14) }} Tags - {{ icon("graph", 14) }} Cluster - {{ icon("magnifying-glass-plus", 14) }} Landtag-Suche + {% if is_authenticated %}{{ icon("bookmark-simple", 14) }} Merkliste{% endif %} + {{ icon("tag", 14) }} Tags + {% if is_admin %}{{ icon("graph", 14) }} Cluster{% endif %} + {% if is_authenticated %}{{ icon("magnifying-glass-plus", 14) }} Landtag-Suche{% endif %}