diff --git a/app.py b/app.py index a67d020..31aeb41 100644 --- a/app.py +++ b/app.py @@ -3830,6 +3830,11 @@ def archive_device(device_id: str) -> bool: aus fetch_devices_for_organization()/fetch_all_devices(), Datensatz und Historie (hardware_snapshots, device_merkmal_events) bleiben in der DB erhalten. + + Seit 23.10.2026 zugleich deprovisioniert (wie deprovision_device()): + Vorher checkte ein gelöschtes Gerät weiter ein und wurde unsichtbar + weiterverwaltet (Liebherr-47, gelöscht 03.10. 13:56, Check-in 16:24). + Eine Neuinstallation (/api/v1/activate) setzt beides wieder zurück. """ with get_database_connection() as conn: @@ -3837,7 +3842,9 @@ def archive_device(device_id: str) -> bool: cur.execute( """ UPDATE devices - SET archived_at = CURRENT_TIMESTAMP + SET archived_at = CURRENT_TIMESTAMP, + deprovisioned_at = COALESCE(deprovisioned_at, CURRENT_TIMESTAMP), + agent_secret_hash = NULL, agent_secret_issued_at = NULL WHERE id = %s AND archived_at IS NULL RETURNING id """, @@ -5736,6 +5743,35 @@ def revoke_enrollment_session(session_id: str): ) return cur.fetchone() is not None +def delete_enrollment_session(organization_id: str, session_id: str) -> str: + """ + GUI-Runde 23.10.2026: "Ausgegebene Medien" verwalten. Gelöscht wird nur, + was ohnehin nicht mehr installieren kann (widerrufen, abgelaufen oder + Kontingent aufgebraucht) - ein noch gültiges Medium muss erst + deaktiviert werden. Nichts verweist per Fremdschlüssel auf die Session; + installierte Geräte bleiben unberührt. Rückgabe: "geloescht", + "nicht_gefunden" oder "noch_gueltig". + """ + + with get_database_connection() as conn: + with conn.cursor() as cur: + cur.execute( + """ + DELETE FROM enrollment_sessions + WHERE id = %s AND organization_id = %s + AND (revoked_at IS NOT NULL OR expires_at <= CURRENT_TIMESTAMP OR devices_used >= max_devices) + RETURNING id + """, + (session_id, organization_id), + ) + if cur.fetchone() is not None: + return "geloescht" + cur.execute( + "SELECT 1 FROM enrollment_sessions WHERE id = %s AND organization_id = %s", + (session_id, organization_id), + ) + return "noch_gueltig" if cur.fetchone() is not None else "nicht_gefunden" + def recharge_enrollment_session(session_id: str, additional_devices: int, expires_at: str | None): """ Phase 5 (Kundenplattform-Selfservice): eine bereits ausgeteilte, @@ -8829,6 +8865,30 @@ def revoke_enrollment_session_endpoint(session_id: str, authorization: str | Non return {"success": True} +@app.delete("/api/v1/organizations/{organization_id}/enrollment-sessions/{session_id}") +def delete_enrollment_session_endpoint( + organization_id: str, session_id: str, authorization: str | None = Header(default=None) +): + if not require_service_token(authorization): + return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."} + + try: + UUID(session_id) + except ValueError: + return {"success": False, "error": "not_found", "message": "Installationsmedium wurde nicht gefunden."} + + ergebnis = delete_enrollment_session(organization_id, session_id) + if ergebnis == "nicht_gefunden": + return {"success": False, "error": "not_found", "message": "Installationsmedium wurde nicht gefunden."} + if ergebnis == "noch_gueltig": + return { + "success": False, "error": "noch_gueltig", + "message": "Das Medium ist noch gültig. Deaktiviere es zuerst, dann kannst du es löschen.", + } + + return {"success": True} + + @app.patch("/api/v1/enrollment-sessions/{session_id}") def recharge_enrollment_session_endpoint( session_id: str,