From b4bd160536129b80d104472537f697123bc1daae Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Wed, 5 Aug 2026 09:32:57 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Flows-Verwaltung=20im=20Admin-Bereich?= =?UTF-8?q?=20(Enrollment=20Sessions,=20Neuger=C3=A4t-Best=C3=A4tigung)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit routers/admin_flows.py: pro Organisation unbestätigte Geräte anzeigen und bestätigen (Bereitstellungsvorlage zuweisen), Enrollment Sessions anlegen/widerrufen. Confirm-Aktion ruft direkt anodes bestehenden resolve-Endpoint auf. Co-Authored-By: Claude Sonnet 5 --- routers/admin_flows.py | 90 ++++++++++++++++++++++++- templates/admin/flows_index.html | 19 ++++++ templates/admin/flows_organisation.html | 79 ++++++++++++++++++++++ 3 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 templates/admin/flows_index.html create mode 100644 templates/admin/flows_organisation.html diff --git a/routers/admin_flows.py b/routers/admin_flows.py index 8aaeae4..5617b54 100644 --- a/routers/admin_flows.py +++ b/routers/admin_flows.py @@ -1,5 +1,7 @@ -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, Depends, Form, Request +from fastapi.responses import RedirectResponse +from anode_client import anode_request from auth import require_admin from templating import templates @@ -9,6 +11,90 @@ router = APIRouter(prefix="/admin/flows") @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/platzhalter.html", {"user": user, "titel": "Flows"} + request, "admin/flows_index.html", {"user": user, "organizations": organizations} ) + + +@router.get("/{organization_id}") +def organisation_flows(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) + + unassigned_result = anode_request( + "GET", f"/api/v1/organizations/{organization_id}/devices/unassigned" + ) + unassigned_devices = unassigned_result.get("devices", []) if unassigned_result.get("success") else [] + + sessions_result = anode_request( + "GET", f"/api/v1/organizations/{organization_id}/enrollment-sessions" + ) + enrollment_sessions = sessions_result.get("enrollment_sessions", []) if sessions_result.get("success") else [] + + templates_result = anode_request( + "GET", f"/api/v1/organizations/{organization_id}/bereitstellungsvorlagen" + ) + bereitstellungsvorlagen = templates_result.get("bereitstellungsvorlagen", []) if templates_result.get("success") else [] + + return templates.TemplateResponse( + request, + "admin/flows_organisation.html", + { + "user": user, + "organization": organization, + "organization_id": organization_id, + "unassigned_devices": unassigned_devices, + "enrollment_sessions": enrollment_sessions, + "bereitstellungsvorlagen": bereitstellungsvorlagen, + }, + ) + + +@router.post("/{organization_id}/geraete/{device_id}/bestaetigen") +def geraet_bestaetigen( + organization_id: str, + device_id: str, + bereitstellungsvorlage_id: str = Form(...), + user: dict = Depends(require_admin), +): + anode_request( + "POST", + f"/api/v1/templates/{bereitstellungsvorlage_id}/resolve", + json={"device_id": device_id}, + ) + + return RedirectResponse(f"/admin/flows/{organization_id}", status_code=303) + + +@router.post("/{organization_id}/enrollment-sessions") +def enrollment_session_anlegen( + organization_id: str, + bereitstellungsvorlage_id: str = Form(...), + max_devices: int = Form(...), + expires_at: str = Form(...), + user: dict = Depends(require_admin), +): + anode_request( + "POST", + f"/api/v1/organizations/{organization_id}/enrollment-sessions", + json={ + "bereitstellungsvorlage_id": bereitstellungsvorlage_id, + "max_devices": max_devices, + "expires_at": expires_at, + }, + ) + + return RedirectResponse(f"/admin/flows/{organization_id}", status_code=303) + + +@router.post("/{organization_id}/enrollment-sessions/{session_id}/widerrufen") +def enrollment_session_widerrufen( + organization_id: str, session_id: str, user: dict = Depends(require_admin) +): + anode_request("POST", f"/api/v1/enrollment-sessions/{session_id}/revoke") + + return RedirectResponse(f"/admin/flows/{organization_id}", status_code=303) diff --git a/templates/admin/flows_index.html b/templates/admin/flows_index.html new file mode 100644 index 0000000..9cbe40b --- /dev/null +++ b/templates/admin/flows_index.html @@ -0,0 +1,19 @@ +{% extends "admin/layout.html" %} +{% block title %}Flows — Tuxflotte Admin{% endblock %} +{% block admin_content %} +

Flows

+

Neugerät-Bestätigung und Enrollment Sessions (ADR-0008) sind je Organisation organisiert.

+
+ + + + {% for org in organizations %} + + + + + {% endfor %} + +
Organisation
{{ org.name }}Öffnen
+
+{% endblock %} diff --git a/templates/admin/flows_organisation.html b/templates/admin/flows_organisation.html new file mode 100644 index 0000000..2accf38 --- /dev/null +++ b/templates/admin/flows_organisation.html @@ -0,0 +1,79 @@ +{% extends "admin/layout.html" %} +{% block title %}Flows: {{ organization.name if organization else organization_id }} — Tuxflotte Admin{% endblock %} +{% block admin_content %} +

← Flows

+

{{ organization.name if organization else organization_id }}

+ +
+

Unbestätigte Geräte

+

Geräte ohne Bereitstellungsvorlagen-Zuweisung. Bestätigung setzt eine Vorlage und schaltet die Installation frei (deckt den serverseitigen Teil aus ADR-0008 ab, nicht das Installer-seitige Warten im Auto-Modus).

+ {% if not unassigned_devices %} +

Keine unbestätigten Geräte.

+ {% else %} +
+ + + {% for device in unassigned_devices %} + + + + + + {% endfor %} + +
{{ device.hostname or device.device_fingerprint }}{{ device.created_at }} +
+ + +
+
+
+ {% endif %} +
+ +
+

Enrollment Sessions

+
+ + + + {% for es in enrollment_sessions %} + + + + + + + + {% endfor %} + +
VorlageGeräteLäuft abStatus
{{ es.bereitstellungsvorlage_label }}{{ es.devices_used }} / {{ es.max_devices }}{{ es.expires_at }}{{ "widerrufen" if es.revoked_at else "aktiv" }} + {% if not es.revoked_at %} +
+ +
+ {% endif %} +
+
+ +
+ Neue Enrollment Session anlegen +
+ + + + +
+
+
+{% endblock %}