kundenplattform/routers/admin_kunden.py
Thomas Stallinger 232d621245 feat: Struktur (OE-Selfservice) + Gruppen-Verwaltung (Admin)
Kundenportal:
- Neue Seite /struktur - OE-Baum (rekursives Jinja-Makro), Anlegen/
  Umbenennen/Löschen (Standard-OE 'Neue Geräte' geschützt), Merkmale-
  Editor pro OE mit Tristate aktiv/inaktiv/nicht festgelegt.
- geraete_liste.html: neue Spalte 'OE', Aktionen-Dropdown um 'In OE
  verschieben' erweitert.
- auftragskatalog.html: zeigt jetzt die Herkunft einer Einstellung an
  (OE/Gruppe/manuell), sofern sie nicht vom Workspace-Default kommt.

Admin (/admin/kunden/{id}):
- Neue Sektion 'Gruppen' - Anlegen/Löschen, verlinkt auf neue
  Detailseite (admin/gruppe_detail.html) mit Merkmale-Editor (wie OE)
  und Geräte-Mitgliedschaft-Checkliste. Bewusst nur intern verwaltbar
  in v1 (siehe Konfigurationsgruppen-Diskussion).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 16:41:04 +02:00

231 lines
9.7 KiB
Python

from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import RedirectResponse
from anode_client import anode_request
from auth import UserCreate, UserManager, get_user_manager, require_admin
from db import get_database_connection
from templating import templates
router = APIRouter(prefix="/admin/kunden")
def fetch_benutzer_fuer_organisation(organization_id: str) -> list[dict]:
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT email, is_superuser, created_at FROM benutzer WHERE organization_id = %s ORDER BY email",
(organization_id,),
)
return [
{"email": email, "is_superuser": is_superuser, "created_at": created_at}
for email, is_superuser, created_at in cur.fetchall()
]
@router.get("")
def index(request: Request, user: dict = Depends(require_admin)):
result = anode_request("GET", "/api/v1/organizations")
organizations = result.get("organizations", []) if result.get("success") else []
return templates.TemplateResponse(
request, "admin/kunden_index.html", {"user": user, "organizations": organizations}
)
@router.post("")
def organisation_anlegen(name: str = Form(...), user: dict = Depends(require_admin)):
anode_request("POST", "/api/v1/organizations", json={"name": name})
return RedirectResponse("/admin/kunden", status_code=303)
@router.get("/{organization_id}")
def detail(request: Request, organization_id: str, user: dict = Depends(require_admin)):
orgs_result = anode_request("GET", "/api/v1/organizations")
organizations = orgs_result.get("organizations", []) if orgs_result.get("success") else []
organization = next((o for o in organizations if o["id"] == organization_id), None)
codes_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/activation-codes")
activation_codes = codes_result.get("activation_codes", []) if codes_result.get("success") else []
benutzer = fetch_benutzer_fuer_organisation(organization_id)
vorlagen_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/bereitstellungsvorlagen")
bereitstellungsvorlagen = vorlagen_result.get("bereitstellungsvorlagen", []) if vorlagen_result.get("success") else []
workspaces_result = anode_request("GET", "/api/v1/workspaces")
workspaces = workspaces_result.get("workspaces", []) if workspaces_result.get("success") else []
backends_result = anode_request("GET", "/api/v1/backends")
backends = backends_result.get("backends", []) if backends_result.get("success") else []
gruppen_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/gruppen")
gruppen = gruppen_result.get("gruppen", []) if gruppen_result.get("success") else []
return templates.TemplateResponse(
request,
"admin/kunden_detail.html",
{
"user": user,
"organization": organization,
"organization_id": organization_id,
"activation_codes": activation_codes,
"benutzer": benutzer,
"bereitstellungsvorlagen": bereitstellungsvorlagen,
"workspaces": workspaces,
"backends": backends,
"gruppen": gruppen,
},
)
@router.post("/{organization_id}/gruppen")
def gruppe_anlegen(organization_id: str, name: str = Form(...), user: dict = Depends(require_admin)):
anode_request("POST", f"/api/v1/organizations/{organization_id}/gruppen", json={"name": name})
return RedirectResponse(f"/admin/kunden/{organization_id}", status_code=303)
@router.post("/{organization_id}/gruppen/{gruppe_id}/loeschen")
def gruppe_loeschen(organization_id: str, gruppe_id: str, user: dict = Depends(require_admin)):
anode_request("DELETE", f"/api/v1/gruppen/{gruppe_id}")
return RedirectResponse(f"/admin/kunden/{organization_id}", status_code=303)
@router.get("/{organization_id}/gruppen/{gruppe_id}")
def gruppe_detail(request: Request, organization_id: str, gruppe_id: str, user: dict = Depends(require_admin)):
gruppen_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/gruppen")
gruppen = gruppen_result.get("gruppen", []) if gruppen_result.get("success") else []
gruppe = next((g for g in gruppen if g["id"] == gruppe_id), None)
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/{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.
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,
"admin/gruppe_detail.html",
{
"user": user,
"organization_id": organization_id,
"gruppe": gruppe,
"kategorien": kategorien,
"devices": devices,
"mitglied_ids": mitglied_ids,
},
)
@router.post("/{organization_id}/gruppen/{gruppe_id}/merkmale/{merkmal_key}/select")
def gruppe_merkmal_select(organization_id: str, gruppe_id: str, merkmal_key: str, user: dict = Depends(require_admin)):
anode_request("POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/select")
return RedirectResponse(f"/admin/kunden/{organization_id}/gruppen/{gruppe_id}", status_code=303)
@router.post("/{organization_id}/gruppen/{gruppe_id}/merkmale/{merkmal_key}/deselect")
def gruppe_merkmal_deselect(organization_id: str, gruppe_id: str, merkmal_key: str, user: dict = Depends(require_admin)):
anode_request("POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/deselect")
return RedirectResponse(f"/admin/kunden/{organization_id}/gruppen/{gruppe_id}", status_code=303)
@router.post("/{organization_id}/gruppen/{gruppe_id}/merkmale/{merkmal_key}/unset")
def gruppe_merkmal_unset(organization_id: str, gruppe_id: str, merkmal_key: str, user: dict = Depends(require_admin)):
anode_request("POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/unset")
return RedirectResponse(f"/admin/kunden/{organization_id}/gruppen/{gruppe_id}", status_code=303)
@router.post("/{organization_id}/gruppen/{gruppe_id}/geraete/{device_id}/hinzufuegen")
def gruppe_geraet_hinzufuegen(organization_id: str, gruppe_id: str, device_id: str, user: dict = Depends(require_admin)):
anode_request("POST", f"/api/v1/devices/{device_id}/gruppen/{gruppe_id}")
return RedirectResponse(f"/admin/kunden/{organization_id}/gruppen/{gruppe_id}", status_code=303)
@router.post("/{organization_id}/gruppen/{gruppe_id}/geraete/{device_id}/entfernen")
def gruppe_geraet_entfernen(organization_id: str, gruppe_id: str, device_id: str, user: dict = Depends(require_admin)):
anode_request("DELETE", f"/api/v1/devices/{device_id}/gruppen/{gruppe_id}")
return RedirectResponse(f"/admin/kunden/{organization_id}/gruppen/{gruppe_id}", status_code=303)
@router.post("/{organization_id}/bereitstellungsvorlagen")
def bereitstellungsvorlage_anlegen(
organization_id: str,
workspace_id: str = Form(...),
backend_id: str = Form(...),
label: str = Form(...),
is_default: str = Form(default=""),
disk_encryption: str = Form(default=""),
root_filesystem: str = Form(default="ext4"),
secure_boot_required: str = Form(default=""),
user: dict = Depends(require_admin),
):
anode_request(
"POST",
f"/api/v1/organizations/{organization_id}/bereitstellungsvorlagen",
json={
"workspace_id": workspace_id,
"backend_id": backend_id,
"label": label,
"is_default": is_default == "on",
"disk_encryption": disk_encryption == "on",
"root_filesystem": root_filesystem,
"secure_boot_required": secure_boot_required == "on",
},
)
return RedirectResponse(f"/admin/kunden/{organization_id}", status_code=303)
@router.post("/{organization_id}/activation-codes")
def aktivierungscode_anlegen(organization_id: str, user: dict = Depends(require_admin)):
anode_request("POST", f"/api/v1/organizations/{organization_id}/activation-codes")
return RedirectResponse(f"/admin/kunden/{organization_id}", status_code=303)
@router.post("/{organization_id}/konten")
async def konto_anlegen(
organization_id: str,
email: str = Form(...),
password: str = Form(...),
is_superuser: str = Form(default=""),
user: dict = Depends(require_admin),
user_manager: UserManager = Depends(get_user_manager),
):
await user_manager.create(
UserCreate(
email=email,
password=password,
organization_id=organization_id,
is_superuser=is_superuser == "on",
)
)
return RedirectResponse(f"/admin/kunden/{organization_id}", status_code=303)