# Design Spec: Auth — Öffentliche Routen & Permission-System **Datum:** 2026-04-27 **Status:** Genehmigt --- ## Ziel Öffentliche und geschützte Routen unterscheiden. Geschützte Routen sind durch eine Dependency einfach zu kennzeichnen. Die Landing Page ist ohne Login erreichbar, zeigt aber dem eingeloggten User seine persönlichen Kacheln. Kacheln werden nur angezeigt, wenn der aktuelle User (oder Anonymous) berechtigt ist. --- ## Permission-Modell `permission` auf einem Modul ist eine `list[str]`. Zugriff wird gewährt wenn der User **mindestens einen** Wert erfüllt (OR-Logik). | Wert | Bedingung | |---|---| | `"public"` | immer True (auch anonyme User) | | `"authenticated"` | User ist eingeloggt | | `"admin"` | `user.is_admin == True` | | `""` | User ist Mitglied dieser Gruppe (Part 2) | Modul-Metadaten (slug, icon, name, description, status) bleiben als Code-Artefakt im `MODULES`-Dict. Berechtigungen wandern erst in die DB wenn Gruppen in Part 2 implementiert werden — `check_permission` ist der einzige Ort der dann angepasst werden muss. --- ## Neue/geänderte Komponenten ### `app/modules/auth/dependencies.py` Drei Ergänzungen: **`get_current_user_optional`** — gibt `User | None` zurück, wirft nie eine Exception. Für öffentliche Routen die optional Userinfo brauchen. ```python async def get_current_user_optional( request: Request, db: Session = Depends(get_db) ) -> User | None: token = get_token_from_request(request) if not token: return None payload = decode_token(token) if not payload: return None return get_user(db, payload.get("sub", "")) ``` **`check_permission`** — reine Funktion, kein FastAPI-Kontext: ```python def check_permission(user: User | None, permissions: list[str]) -> bool: if "public" in permissions: return True if user is None: return False if "authenticated" in permissions: return True if "admin" in permissions and user.is_admin: return True return False # group check: Part 2 ``` **`require_permission`** — Dependency-Factory: ```python def require_permission(permissions: list[str]): async def _dep( request: Request, db: Session = Depends(get_db) ) -> User | None: user = await get_current_user_optional(request, db) if not check_permission(user, permissions): raise RequiresLoginException() return user return _dep ``` Bestehende `get_current_user` und `require_admin` bleiben für Kompatibilität erhalten, delegieren aber intern an `require_permission`: - `get_current_user` = `require_permission(["authenticated"])` - `require_admin` = `require_permission(["admin"])` ### `app/main.py` — MODULES-Registry `MODULES` bekommt `slug` und `permission`: ```python MODULES = [ { "slug": "rss", "icon": "📡", "name": "RSS-Feed Server", "description": "Aggregiert und verteilt Neuigkeiten der Fakultät als standardkonformen RSS 2.0 Feed.", "status": "active", "permission": ["authenticated"], }, { "slug": "kalender", "icon": "📅", "name": "Kalender", "description": "Veranstaltungen, Prüfungstermine und Fristen im iCal-Format.", "status": "planned", "permission": ["authenticated"], }, # ... ] ``` Aktive Module (`status == "active"`) werden beim App-Start automatisch registriert: ```python import importlib for module in MODULES: if module["status"] == "active": mod = importlib.import_module(f"app.modules.{module['slug']}.router") app.include_router(mod.router, prefix=f"/{module['slug']}") ``` `GET /` wird öffentlich — `get_current_user_optional` statt `get_current_user`: ```python @app.get("/", response_class=HTMLResponse) async def root(request: Request, current_user=Depends(get_current_user_optional)): visible = [m for m in MODULES if check_permission(current_user, m["permission"])] return templates.TemplateResponse(request, "index.html", { "nav_items": NAV_ITEMS, "modules": visible, "db_mode": _db_mode(), "app_version": "0.1.0", "current_user": current_user, }) ``` ### `app/templates/base.html` — Navbar Navbar zeigt conditional: - **Eingeloggt:** Username + Logout-Button - **Nicht eingeloggt:** Anmelden-Button (→ `/auth/login`) ```html {% if current_user %} {{ current_user.username }} Abmelden {% else %} Anmelden {% endif %} ``` --- ## Neue Modul-Konvention Ein Entwickler der ein neues Modul anlegt: 1. Verzeichnis `app/modules//` mit `__init__.py` und `router.py` 2. `router.py` schützt alle Routen über den Router: ```python router = APIRouter(dependencies=[Depends(require_permission(["authenticated"]))]) ``` Für feinere Kontrolle einzelner Routen: ```python @router.get("/secret", dependencies=[Depends(require_permission(["admin"]))]) ``` 3. Eintrag in `MODULES` in `main.py` mit `status: "active"` und passendem `permission` --- ## Out of Scope - Gruppen-Tabelle und Gruppen-Verwaltung (→ Part 2) - Modul-Permissions in der DB (→ Part 2, zusammen mit Gruppen) - Admin-UI für Berechtigungsvergabe - Per-Objekt-Permissions (Row-Level Security)