diff --git a/app.py b/app.py index 08ccb2d..d59679a 100644 --- a/app.py +++ b/app.py @@ -8,6 +8,7 @@ from routers import ( admin_kunden, admin_technik, geraete, + gruppen, installationsmedium, organisation, struktur, @@ -21,6 +22,7 @@ app.include_router(auth.router) app.include_router(geraete.router) app.include_router(organisation.router) app.include_router(struktur.router) +app.include_router(gruppen.router) app.include_router(installationsmedium.router) app.include_router(admin_technik.router) app.include_router(admin_geraete.router) diff --git a/routers/gruppen.py b/routers/gruppen.py new file mode 100644 index 0000000..d12f374 --- /dev/null +++ b/routers/gruppen.py @@ -0,0 +1,180 @@ +from fastapi import APIRouter, Depends, Form, HTTPException, Request +from fastapi.responses import RedirectResponse + +from anode_client import anode_request +from auth import get_current_user +from templating import templates + + +router = APIRouter() + + +def fetch_gruppen(organization_id: str) -> list[dict]: + result = anode_request("GET", f"/api/v1/organizations/{organization_id}/gruppen") + return result.get("gruppen", []) if result.get("success") else [] + + +def find_gruppe_in_organization(gruppe_id: str, organization_id: str) -> dict | None: + """ + Besitz-Validierung analog zu find_oe_in_organization() in + routers/struktur.py (siehe ADR-0011): eine gruppe_id aus der URL wird + nur akzeptiert, wenn sie tatsächlich in der Gruppenliste der eigenen + Organisation auftaucht. + """ + + for gruppe in fetch_gruppen(organization_id): + if gruppe["id"] == gruppe_id: + return gruppe + + return None + + +@router.get("/gruppen") +def gruppen_liste(request: Request, user=Depends(get_current_user)): + gruppen = fetch_gruppen(user.organization_id) + + return templates.TemplateResponse(request, "gruppen_liste.html", {"user": user, "gruppen": gruppen}) + + +@router.post("/gruppen") +def gruppe_anlegen(name: str = Form(...), user=Depends(get_current_user)): + anode_request("POST", f"/api/v1/organizations/{user.organization_id}/gruppen", json={"name": name}) + + return RedirectResponse("/gruppen", status_code=303) + + +@router.get("/gruppen/{gruppe_id}/umbenennen") +def gruppe_umbenennen_form(request: Request, gruppe_id: str, user=Depends(get_current_user)): + gruppe = find_gruppe_in_organization(gruppe_id, user.organization_id) + + if gruppe is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + return templates.TemplateResponse(request, "gruppe_umbenennen.html", {"user": user, "gruppe": gruppe}) + + +@router.post("/gruppen/{gruppe_id}/umbenennen") +def gruppe_umbenennen(gruppe_id: str, name: str = Form(...), user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + anode_request("PATCH", f"/api/v1/gruppen/{gruppe_id}", json={"name": name}) + + return RedirectResponse("/gruppen", status_code=303) + + +@router.get("/gruppen/{gruppe_id}/loeschen") +def gruppe_loeschen_form(request: Request, gruppe_id: str, user=Depends(get_current_user)): + gruppe = find_gruppe_in_organization(gruppe_id, user.organization_id) + + if gruppe is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + return templates.TemplateResponse(request, "gruppe_loeschen.html", {"user": user, "gruppe": gruppe}) + + +@router.post("/gruppen/{gruppe_id}/loeschen") +def gruppe_loeschen(gruppe_id: str, user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + anode_request("DELETE", f"/api/v1/gruppen/{gruppe_id}") + + return RedirectResponse("/gruppen", status_code=303) + + +@router.get("/gruppen/{gruppe_id}") +def gruppe_detail(request: Request, gruppe_id: str, user=Depends(get_current_user)): + gruppe = find_gruppe_in_organization(gruppe_id, user.organization_id) + + if gruppe is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + merkmale_result = anode_request("GET", f"/api/v1/gruppen/{gruppe_id}/merkmale") + merkmale = merkmale_result.get("merkmale", []) if merkmale_result.get("success") else [] + + kategorien: dict[str, list[dict]] = {} + for eintrag in merkmale: + kategorie_name = eintrag["kategorie"]["name"] if eintrag["kategorie"] else "Ohne Kategorie" + kategorien.setdefault(kategorie_name, []).append(eintrag) + + devices_result = anode_request("GET", f"/api/v1/organizations/{user.organization_id}/devices") + devices = devices_result.get("devices", []) if devices_result.get("success") else [] + + # anode bietet keinen direkten "Geräte einer Gruppe"-Endpunkt an, nur + # "Gruppen eines Geräts" (GET /devices/{id}/gruppen) - daher hier für + # jedes Gerät der Organisation dessen Gruppen abfragen und filtern + # (gleiches Muster wie admin_kunden.py:gruppe_detail()). + mitglied_ids = set() + for device in devices: + dg_result = anode_request("GET", f"/api/v1/devices/{device['id']}/gruppen") + device_gruppen = dg_result.get("gruppen", []) if dg_result.get("success") else [] + if any(g["id"] == gruppe_id for g in device_gruppen): + mitglied_ids.add(device["id"]) + + return templates.TemplateResponse( + request, + "gruppe_detail.html", + { + "user": user, + "gruppe": gruppe, + "kategorien": kategorien, + "devices": devices, + "mitglied_ids": mitglied_ids, + }, + ) + + +@router.post("/gruppen/{gruppe_id}/merkmale/{merkmal_key}/select") +def gruppe_merkmal_select(gruppe_id: str, merkmal_key: str, user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + anode_request("POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/select") + + return RedirectResponse(f"/gruppen/{gruppe_id}", status_code=303) + + +@router.post("/gruppen/{gruppe_id}/merkmale/{merkmal_key}/deselect") +def gruppe_merkmal_deselect(gruppe_id: str, merkmal_key: str, user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + anode_request("POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/deselect") + + return RedirectResponse(f"/gruppen/{gruppe_id}", status_code=303) + + +@router.post("/gruppen/{gruppe_id}/merkmale/{merkmal_key}/unset") +def gruppe_merkmal_unset(gruppe_id: str, merkmal_key: str, user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + anode_request("POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/unset") + + return RedirectResponse(f"/gruppen/{gruppe_id}", status_code=303) + + +@router.post("/gruppen/{gruppe_id}/geraete/{device_id}/hinzufuegen") +def gruppe_geraet_hinzufuegen(gruppe_id: str, device_id: str, user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + # Besitz-Validierung fürs Gerät reicht hier über den bestehenden + # find_device_in_organization()-Umweg nicht direkt, aber anode prüft + # bei add_device_to_gruppe() bereits serverseitig, dass Gerät und + # Gruppe zur selben Organisation gehören (siehe app.py) - ein Zugriff + # auf ein fremdes device_id würde dort mit invalid_membership abgelehnt. + anode_request("POST", f"/api/v1/devices/{device_id}/gruppen/{gruppe_id}") + + return RedirectResponse(f"/gruppen/{gruppe_id}", status_code=303) + + +@router.post("/gruppen/{gruppe_id}/geraete/{device_id}/entfernen") +def gruppe_geraet_entfernen(gruppe_id: str, device_id: str, user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + anode_request("DELETE", f"/api/v1/devices/{device_id}/gruppen/{gruppe_id}") + + return RedirectResponse(f"/gruppen/{gruppe_id}", status_code=303) diff --git a/templates/base.html b/templates/base.html index 42adf5d..081806a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -20,6 +20,7 @@