feat: Flows-Verwaltung im Admin-Bereich (Enrollment Sessions, Neugerät-Bestätigung)
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 <noreply@anthropic.com>
This commit is contained in:
parent
eb9fc6b8a7
commit
b4bd160536
@ -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)
|
||||
|
||||
19
templates/admin/flows_index.html
Normal file
19
templates/admin/flows_index.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "admin/layout.html" %}
|
||||
{% block title %}Flows — Tuxflotte Admin{% endblock %}
|
||||
{% block admin_content %}
|
||||
<h2>Flows</h2>
|
||||
<p><small>Neugerät-Bestätigung und Enrollment Sessions (ADR-0008) sind je Organisation organisiert.</small></p>
|
||||
<figure>
|
||||
<table>
|
||||
<thead><tr><th>Organisation</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for org in organizations %}
|
||||
<tr>
|
||||
<td>{{ org.name }}</td>
|
||||
<td><a href="/admin/flows/{{ org.id }}" role="button" class="outline">Öffnen</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
{% endblock %}
|
||||
79
templates/admin/flows_organisation.html
Normal file
79
templates/admin/flows_organisation.html
Normal file
@ -0,0 +1,79 @@
|
||||
{% extends "admin/layout.html" %}
|
||||
{% block title %}Flows: {{ organization.name if organization else organization_id }} — Tuxflotte Admin{% endblock %}
|
||||
{% block admin_content %}
|
||||
<p><a href="/admin/flows">← Flows</a></p>
|
||||
<h2>{{ organization.name if organization else organization_id }}</h2>
|
||||
|
||||
<section>
|
||||
<h3>Unbestätigte Geräte</h3>
|
||||
<p><small>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).</small></p>
|
||||
{% if not unassigned_devices %}
|
||||
<p>Keine unbestätigten Geräte.</p>
|
||||
{% else %}
|
||||
<figure>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for device in unassigned_devices %}
|
||||
<tr>
|
||||
<td>{{ device.hostname or device.device_fingerprint }}</td>
|
||||
<td>{{ device.created_at }}</td>
|
||||
<td>
|
||||
<form method="post" action="/admin/flows/{{ organization_id }}/geraete/{{ device.id }}/bestaetigen">
|
||||
<select name="bereitstellungsvorlage_id">
|
||||
{% for bv in bereitstellungsvorlagen %}
|
||||
<option value="{{ bv.id }}" {{ "selected" if bv.is_default else "" }}>{{ bv.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit">Bestätigen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Enrollment Sessions</h3>
|
||||
<figure>
|
||||
<table>
|
||||
<thead><tr><th>Vorlage</th><th>Geräte</th><th>Läuft ab</th><th>Status</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for es in enrollment_sessions %}
|
||||
<tr>
|
||||
<td>{{ es.bereitstellungsvorlage_label }}</td>
|
||||
<td>{{ es.devices_used }} / {{ es.max_devices }}</td>
|
||||
<td>{{ es.expires_at }}</td>
|
||||
<td>{{ "widerrufen" if es.revoked_at else "aktiv" }}</td>
|
||||
<td>
|
||||
{% if not es.revoked_at %}
|
||||
<form method="post" action="/admin/flows/{{ organization_id }}/enrollment-sessions/{{ es.id }}/widerrufen">
|
||||
<button type="submit" class="secondary outline">Widerrufen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
|
||||
<details>
|
||||
<summary role="button" class="secondary outline">Neue Enrollment Session anlegen</summary>
|
||||
<form method="post" action="/admin/flows/{{ organization_id }}/enrollment-sessions">
|
||||
<label>Bereitstellungsvorlage
|
||||
<select name="bereitstellungsvorlage_id">
|
||||
{% for bv in bereitstellungsvorlagen %}
|
||||
<option value="{{ bv.id }}">{{ bv.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>Max. Geräteanzahl<input type="number" name="max_devices" value="10" min="1" required></label>
|
||||
<label>Läuft ab am<input type="datetime-local" name="expires_at" required></label>
|
||||
<button type="submit">Anlegen</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user