Gerätegruppen mit alternativem Workspace, device_merkmale abgelöst
- Migration 0038: gruppen.workspace_id (nullable FK), device_merkmale
Tabelle entfernt (keine Datenzeilen auf anode, geprüft vor Drop).
- set_gruppe_workspace()/clear_gruppe_workspace(): weist einer
Gerätegruppe einen alternativen Workspace zu, ersetzt die komplette
Baseline für Mitgliedsgeräte statt nur einzelner Merkmale. Prüft
Organisationszugehörigkeit des Workspace (global oder eigene Org) und
die Exklusivitätsregel (ein Gerät nur in einer workspace-tragenden
Gruppe gleichzeitig) - sowohl beim Zuweisen als auch beim Hinzufügen
eines Geräts zu einer bereits workspace-tragenden Gruppe.
- resolve_effective_workspace_id(): neue Auflösungsstufe zwischen
Bereitstellungsvorlage und OE-/Gruppen-Merkmal-Overrides.
- fetch_auftragskatalog_state()/_listing(): device_merkmale-Override-Ebene
entfernt (gewann bisher immer), select_auftrag/deselect_auftrag-
Endpunkte + set_auftrag_selection() entfernt. get_device_auftragskatalog
bleibt als reine Ist-Zustand-Anzeige.
- Neuer Endpunkt GET /api/v1/organizations/{id}/workspaces (global +
eigene, org-gefiltert - fetch_workspaces() bleibt admin-only, würde
sonst fremde private Workspaces preisgeben).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLk93dhDMrkPXefVdknMV9
This commit is contained in:
1 parent
b270103eab
commit
10d31f8cf3
3 files changed
+312
-137
No files matched your search
@@ -283,6 +283,10 @@ class UpdateGruppeRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class SetGruppeWorkspaceRequest(BaseModel):
|
||||
workspace_id: str
|
||||
|
||||
|
||||
def write_log(entry: dict) -> None:
|
||||
with LOG_PATH.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||
@@ -1043,6 +1047,32 @@ def resolve_katalog_overrides(device_id: str) -> dict:
|
||||
|
||||
return ergebnis
|
||||
|
||||
def resolve_effective_workspace_id(cur, device_id: str, vorlage_workspace_id: str) -> str:
|
||||
"""
|
||||
Ermittelt den effektiv geltenden Workspace eines Geräts: normalerweise
|
||||
der bei der Installation zugewiesene (über die Bereitstellungsvorlage),
|
||||
außer eine Gerätegruppe des Geräts trägt einen eigenen Workspace - dann
|
||||
gewinnt dieser (siehe set_gruppe_workspace(), Design-Runde 10.09.2026).
|
||||
Das Backend/die Distro bleiben davon unberührt, die ändert sich nie
|
||||
nachträglich - nur welcher Merkmal-Grundbestand gilt. Die
|
||||
Exklusivitätsregel beim Zuweisen sorgt dafür, dass hier höchstens eine
|
||||
Zeile zurückkommt.
|
||||
"""
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT g.workspace_id
|
||||
FROM device_gruppen dg
|
||||
JOIN gruppen g ON g.id = dg.gruppe_id
|
||||
WHERE dg.device_id = %s AND g.workspace_id IS NOT NULL
|
||||
LIMIT 1
|
||||
""",
|
||||
(device_id,),
|
||||
)
|
||||
gruppen_row = cur.fetchone()
|
||||
|
||||
return gruppen_row[0] if gruppen_row is not None else vorlage_workspace_id
|
||||
|
||||
def fetch_auftragskatalog_state(device_id: str):
|
||||
"""
|
||||
Voller Soll-Zustand (present/absent) aller katalogfähigen Merkmale für
|
||||
@@ -1050,9 +1080,13 @@ def fetch_auftragskatalog_state(device_id: str):
|
||||
Konfigurationsgruppen-Plan). Zustandslos aus der aktuellen Auswahl
|
||||
abgeleitet, keine Historie nötig.
|
||||
|
||||
Auflösungsreihenfolge: Workspace-Default → OE-/Gruppen-Zwischenschicht
|
||||
(resolve_katalog_overrides) → device_merkmale-Override, der gewinnt
|
||||
immer, unabhängig davon was OE-Kette oder Gruppen sagen.
|
||||
Auflösungsreihenfolge: Workspace-Default (ggf. durch eine
|
||||
workspace-tragende Gerätegruppe ersetzt, siehe
|
||||
resolve_effective_workspace_id()) → OE-/Gruppen-Merkmal-Zwischenschicht
|
||||
(resolve_katalog_overrides). Die frühere device_merkmale-Override-Ebene
|
||||
(gewann immer) wurde am 10.09.2026 abgelöst - Einzelgerät-Ausnahmen
|
||||
laufen jetzt über eine Ein-Geräte-Gruppe, also über genau dieselbe
|
||||
OE-/Gruppen-Zwischenschicht wie jede andere Gruppe auch.
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
@@ -1071,38 +1105,28 @@ def fetch_auftragskatalog_state(device_id: str):
|
||||
if assignment_row is None:
|
||||
return []
|
||||
|
||||
workspace_id, backend_id = assignment_row
|
||||
vorlage_workspace_id, backend_id = assignment_row
|
||||
workspace_id = resolve_effective_workspace_id(cur, device_id, vorlage_workspace_id)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.id, m.key, bp.ansible_role,
|
||||
dm.aktiv,
|
||||
(wm.merkmal_id IS NOT NULL) AS workspace_default,
|
||||
COALESCE(dm.optionen, '{}'::jsonb)
|
||||
(wm.merkmal_id IS NOT NULL) AS workspace_default
|
||||
FROM merkmale m
|
||||
JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = %s
|
||||
LEFT JOIN device_merkmale dm
|
||||
ON dm.merkmal_id = m.id AND dm.device_id = %s
|
||||
LEFT JOIN workspace_merkmale wm
|
||||
ON wm.merkmal_id = m.id AND wm.workspace_id = %s
|
||||
WHERE m.im_auftragskatalog
|
||||
""",
|
||||
(backend_id, device_id, workspace_id),
|
||||
(backend_id, workspace_id),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
overrides = resolve_katalog_overrides(device_id)
|
||||
|
||||
ergebnis = []
|
||||
for merkmal_id, key, ansible_role, device_override, workspace_default, device_optionen in rows:
|
||||
# optionen folgen derselben Rangfolge wie aktiv - device_merkmale
|
||||
# gewinnt, sonst die OE-/Gruppen-Quelle, die die aktiv-Entscheidung
|
||||
# getroffen hat, sonst leer (kein Optionen-Konzept auf reiner
|
||||
# Workspace-Default-Ebene, siehe workspace_merkmale-Schema).
|
||||
if device_override is not None:
|
||||
aktiv = device_override
|
||||
optionen = device_optionen
|
||||
elif merkmal_id in overrides:
|
||||
for merkmal_id, key, ansible_role, workspace_default in rows:
|
||||
if merkmal_id in overrides:
|
||||
aktiv, _quelle, _quelle_id, optionen = overrides[merkmal_id]
|
||||
else:
|
||||
aktiv = workspace_default
|
||||
@@ -1140,26 +1164,23 @@ def fetch_auftragskatalog_listing(device_id: str):
|
||||
if assignment_row is None:
|
||||
return None
|
||||
|
||||
workspace_id, backend_id = assignment_row
|
||||
vorlage_workspace_id, backend_id = assignment_row
|
||||
workspace_id = resolve_effective_workspace_id(cur, device_id, vorlage_workspace_id)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.id, m.key, m.name, m.description,
|
||||
k.key, k.name, k.sort_order,
|
||||
dm.aktiv,
|
||||
(wm.merkmal_id IS NOT NULL) AS workspace_default,
|
||||
COALESCE(dm.optionen, '{}'::jsonb)
|
||||
(wm.merkmal_id IS NOT NULL) AS workspace_default
|
||||
FROM merkmale m
|
||||
JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = %s
|
||||
LEFT JOIN kategorien k ON k.id = m.kategorie_id
|
||||
LEFT JOIN device_merkmale dm
|
||||
ON dm.merkmal_id = m.id AND dm.device_id = %s
|
||||
LEFT JOIN workspace_merkmale wm
|
||||
ON wm.merkmal_id = m.id AND wm.workspace_id = %s
|
||||
WHERE m.im_auftragskatalog
|
||||
ORDER BY k.sort_order NULLS LAST, m.name
|
||||
""",
|
||||
(backend_id, device_id, workspace_id),
|
||||
(backend_id, workspace_id),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
@@ -1169,11 +1190,9 @@ def fetch_auftragskatalog_listing(device_id: str):
|
||||
for (
|
||||
merkmal_id, key, name, description,
|
||||
kat_key, kat_name, sort_order,
|
||||
device_override, workspace_default, device_optionen,
|
||||
workspace_default,
|
||||
) in rows:
|
||||
if device_override is not None:
|
||||
aktiv, quelle, quelle_id, optionen = device_override, "geraet", None, device_optionen
|
||||
elif merkmal_id in overrides:
|
||||
if merkmal_id in overrides:
|
||||
aktiv, quelle, quelle_id, optionen = overrides[merkmal_id]
|
||||
else:
|
||||
aktiv, quelle, quelle_id, optionen = workspace_default, "workspace", None, {}
|
||||
@@ -1200,62 +1219,6 @@ def fetch_auftragskatalog_listing(device_id: str):
|
||||
|
||||
return ergebnis
|
||||
|
||||
def set_auftrag_selection(device_id: str, merkmal_key: str, aktiv: bool, optionen: dict):
|
||||
"""
|
||||
Setzt die geräteweise Auswahl eines katalogfähigen Merkmals (Upsert in
|
||||
device_merkmale) und protokolliert die Änderung als selected/deselected-
|
||||
Event. Gibt False zurück, wenn kein katalogfähiges Merkmal mit diesem Key
|
||||
existiert (unbekannter Key oder im_auftragskatalog = false).
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id FROM merkmale WHERE key = %s AND im_auftragskatalog",
|
||||
(merkmal_key,),
|
||||
)
|
||||
merkmal_row = cur.fetchone()
|
||||
|
||||
if merkmal_row is None:
|
||||
return False
|
||||
|
||||
merkmal_id = merkmal_row[0]
|
||||
|
||||
# Bestehende optionen holen und mit den neu eingegebenen
|
||||
# zusammenführen (siehe merge_optionen()) - sonst würde ein leer
|
||||
# gelassenes Passwort-Feld im Formular ein zuvor gesetztes
|
||||
# Passwort löschen, statt es unverändert zu lassen.
|
||||
cur.execute(
|
||||
"SELECT optionen FROM device_merkmale WHERE device_id = %s AND merkmal_id = %s",
|
||||
(device_id, merkmal_id),
|
||||
)
|
||||
bestehend_row = cur.fetchone()
|
||||
optionen = encrypt_sensitive_optionen(
|
||||
merge_optionen(bestehend_row[0] if bestehend_row else {}, optionen)
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO device_merkmale (id, device_id, merkmal_id, aktiv, optionen)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
ON CONFLICT (device_id, merkmal_id) DO UPDATE
|
||||
SET aktiv = EXCLUDED.aktiv,
|
||||
optionen = EXCLUDED.optionen,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""",
|
||||
(uuid4(), device_id, merkmal_id, aktiv, Jsonb(optionen)),
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO device_merkmal_events (id, device_id, merkmal_id, event_type)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""",
|
||||
(uuid4(), device_id, merkmal_id, "selected" if aktiv else "deselected"),
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def fetch_organisationseinheiten(organization_id: str):
|
||||
"""
|
||||
Flache Liste aller OEs einer Organisation (inkl. parent_id) - der Baum
|
||||
@@ -1532,10 +1495,24 @@ def fetch_gruppen(organization_id: str):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id, name FROM gruppen WHERE organization_id = %s ORDER BY name",
|
||||
"""
|
||||
SELECT g.id, g.name, g.workspace_id, w.name
|
||||
FROM gruppen g
|
||||
LEFT JOIN workspaces w ON w.id = g.workspace_id
|
||||
WHERE g.organization_id = %s
|
||||
ORDER BY g.name
|
||||
""",
|
||||
(organization_id,),
|
||||
)
|
||||
return [{"id": str(gruppe_id), "name": name} for gruppe_id, name in cur.fetchall()]
|
||||
return [
|
||||
{
|
||||
"id": str(gruppe_id),
|
||||
"name": name,
|
||||
"workspace_id": str(workspace_id) if workspace_id else None,
|
||||
"workspace_name": workspace_name,
|
||||
}
|
||||
for gruppe_id, name, workspace_id, workspace_name in cur.fetchall()
|
||||
]
|
||||
|
||||
def create_gruppe(organization_id: str, name: str):
|
||||
gruppe_id = uuid4()
|
||||
@@ -1569,6 +1546,87 @@ def delete_gruppe(gruppe_id: str):
|
||||
cur.execute("DELETE FROM gruppen WHERE id = %s RETURNING id", (gruppe_id,))
|
||||
return cur.fetchone() is not None
|
||||
|
||||
def set_gruppe_workspace(gruppe_id: str, workspace_id: str):
|
||||
"""
|
||||
Weist einer Gerätegruppe einen alternativen Workspace zu (Design-Runde
|
||||
10.09.2026, siehe project_phase2_app_store_merkmale-Memory) - ersetzt
|
||||
für Mitgliedsgeräte die komplette Workspace-Baseline (nicht nur
|
||||
einzelne Merkmale, dafür bleibt gruppen_merkmale zuständig). Ein
|
||||
Gerät kann gleichzeitig nur in EINER workspace-tragenden Gerätegruppe
|
||||
sein (bewusste Nutzer-Entscheidung, verhindert widersprüchliche
|
||||
Workspace-Wahl statt einer Prioritätsregel) - deshalb hier zusätzlich
|
||||
zur Organisationszugehörigkeit geprüft: kein Mitgliedsgerät der
|
||||
Gruppe darf schon in einer ANDEREN workspace-tragenden Gruppe sein.
|
||||
|
||||
Rückgabe: (True, None) bei Erfolg, sonst (False, Fehlercode).
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT organization_id FROM gruppen WHERE id = %s",
|
||||
(gruppe_id,),
|
||||
)
|
||||
gruppe_row = cur.fetchone()
|
||||
|
||||
if gruppe_row is None:
|
||||
return False, "gruppe_not_found"
|
||||
|
||||
gruppe_org_id = gruppe_row[0]
|
||||
|
||||
cur.execute(
|
||||
"SELECT organization_id FROM workspaces WHERE id = %s",
|
||||
(workspace_id,),
|
||||
)
|
||||
workspace_row = cur.fetchone()
|
||||
|
||||
if workspace_row is None:
|
||||
return False, "workspace_not_found"
|
||||
|
||||
# NULL organization_id = globaler Standard-Workspace (siehe
|
||||
# workspaces-Schema), den jede Organisation nutzen darf. Ist er
|
||||
# gesetzt, muss er zur eigenen Organisation gehören - ein
|
||||
# fremder, privater Workspace einer anderen Organisation darf
|
||||
# nicht ausgewählt werden können.
|
||||
workspace_org_id = workspace_row[0]
|
||||
if workspace_org_id is not None and workspace_org_id != gruppe_org_id:
|
||||
return False, "workspace_wrong_organization"
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT DISTINCT d.id, d.hostname, g2.name
|
||||
FROM device_gruppen dg
|
||||
JOIN devices d ON d.id = dg.device_id
|
||||
JOIN gruppen g2 ON g2.id = dg.gruppe_id
|
||||
WHERE dg.device_id IN (
|
||||
SELECT device_id FROM device_gruppen WHERE gruppe_id = %s
|
||||
)
|
||||
AND g2.id != %s
|
||||
AND g2.workspace_id IS NOT NULL
|
||||
""",
|
||||
(gruppe_id, gruppe_id),
|
||||
)
|
||||
konflikt_row = cur.fetchone()
|
||||
|
||||
if konflikt_row is not None:
|
||||
return False, "workspace_conflict"
|
||||
|
||||
cur.execute(
|
||||
"UPDATE gruppen SET workspace_id = %s WHERE id = %s",
|
||||
(workspace_id, gruppe_id),
|
||||
)
|
||||
|
||||
return True, None
|
||||
|
||||
def clear_gruppe_workspace(gruppe_id: str):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE gruppen SET workspace_id = NULL WHERE id = %s RETURNING id",
|
||||
(gruppe_id,),
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
|
||||
def set_gruppe_auswahl(gruppe_id: str, merkmal_key: str, aktiv: bool, optionen: dict):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
@@ -1659,14 +1717,17 @@ def fetch_gruppe_merkmale_listing(gruppe_id: str):
|
||||
def add_device_to_gruppe(device_id: str, gruppe_id: str):
|
||||
"""
|
||||
Fügt ein Gerät einer Gruppe hinzu (idempotent). Gibt False zurück, wenn
|
||||
Gerät und Gruppe nicht zur selben Organisation gehören.
|
||||
Gerät und Gruppe nicht zur selben Organisation gehören, oder "conflict",
|
||||
wenn die Zielgruppe einen Workspace trägt und das Gerät bereits in einer
|
||||
ANDEREN workspace-tragenden Gruppe ist (siehe set_gruppe_workspace() für
|
||||
die Begründung der Exklusivitätsregel).
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT d.organization_id, g.organization_id
|
||||
SELECT d.organization_id, g.organization_id, g.workspace_id
|
||||
FROM devices d, gruppen g
|
||||
WHERE d.id = %s AND g.id = %s
|
||||
""",
|
||||
@@ -1677,6 +1738,21 @@ def add_device_to_gruppe(device_id: str, gruppe_id: str):
|
||||
if row is None or row[0] != row[1]:
|
||||
return False
|
||||
|
||||
ziel_workspace_id = row[2]
|
||||
|
||||
if ziel_workspace_id is not None:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM device_gruppen dg
|
||||
JOIN gruppen g ON g.id = dg.gruppe_id
|
||||
WHERE dg.device_id = %s AND g.workspace_id IS NOT NULL AND g.id != %s
|
||||
""",
|
||||
(device_id, gruppe_id),
|
||||
)
|
||||
if cur.fetchone() is not None:
|
||||
return "conflict"
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO device_gruppen (device_id, gruppe_id)
|
||||
@@ -2322,6 +2398,32 @@ def fetch_workspaces():
|
||||
) in cur.fetchall()
|
||||
]
|
||||
|
||||
def fetch_available_workspaces_for_organization(organization_id: str):
|
||||
"""
|
||||
Workspaces, die eine Organisation für ihre Gerätegruppen wählen darf:
|
||||
globale Standard-Workspaces (organization_id IS NULL, von uns
|
||||
gepflegt) plus die eigenen. Bewusst NICHT fetch_workspaces() - die
|
||||
liefert workspaces aller Organisationen (nur für die Admin-Techniker-
|
||||
Ansicht gedacht), das würde im Kundenportal fremde, private Workspace-
|
||||
Namen preisgeben.
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, key, name, description
|
||||
FROM workspaces
|
||||
WHERE organization_id IS NULL OR organization_id = %s
|
||||
ORDER BY (organization_id IS NULL) DESC, name
|
||||
""",
|
||||
(organization_id,),
|
||||
)
|
||||
return [
|
||||
{"id": str(wid), "key": key, "name": name, "description": description}
|
||||
for wid, key, name, description in cur.fetchall()
|
||||
]
|
||||
|
||||
def create_workspace(key: str, name: str, description: str | None, organization_id: str | None):
|
||||
workspace_id = uuid4()
|
||||
|
||||
@@ -4199,52 +4301,11 @@ def get_device_auftragskatalog(
|
||||
return {"success": True, "auftragskatalog": katalog}
|
||||
|
||||
|
||||
@app.post("/api/v1/devices/{device_id}/auftragskatalog/{merkmal_key}/select")
|
||||
def select_auftrag(
|
||||
device_id: str,
|
||||
merkmal_key: str,
|
||||
payload: AuftragSelectRequest,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "unauthorized",
|
||||
"message": "Fehlendes oder ungültiges Admin-Token.",
|
||||
}
|
||||
|
||||
if not set_auftrag_selection(device_id, merkmal_key, True, payload.optionen):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "unknown_merkmal",
|
||||
"message": f"Kein katalogfähiges Merkmal mit Key: {merkmal_key}",
|
||||
}
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.post("/api/v1/devices/{device_id}/auftragskatalog/{merkmal_key}/deselect")
|
||||
def deselect_auftrag(
|
||||
device_id: str,
|
||||
merkmal_key: str,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "unauthorized",
|
||||
"message": "Fehlendes oder ungültiges Admin-Token.",
|
||||
}
|
||||
|
||||
if not set_auftrag_selection(device_id, merkmal_key, False, {}):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "unknown_merkmal",
|
||||
"message": f"Kein katalogfähiges Merkmal mit Key: {merkmal_key}",
|
||||
}
|
||||
|
||||
return {"success": True}
|
||||
|
||||
# Direkte Geräte-Einzel-Auswahl (select/deselect) wurde am 10.09.2026
|
||||
# abgelöst - device_merkmale ist weg, Einzelgerät-Ausnahmen laufen jetzt
|
||||
# über eine Ein-Geräte-Gruppe (siehe set_gruppe_workspace() und
|
||||
# gruppen_merkmale-Endpunkte oben). get_device_auftragskatalog() bleibt als
|
||||
# reine Ist-Zustand-Anzeige bestehen.
|
||||
|
||||
# --- Organisationseinheiten (OEs) ---
|
||||
|
||||
@@ -4455,6 +4516,53 @@ def unset_gruppe_merkmal(gruppe_id: str, merkmal_key: str, authorization: str |
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.get("/api/v1/organizations/{organization_id}/workspaces")
|
||||
def list_available_workspaces(organization_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."}
|
||||
|
||||
return {"success": True, "workspaces": fetch_available_workspaces_for_organization(organization_id)}
|
||||
|
||||
|
||||
@app.post("/api/v1/gruppen/{gruppe_id}/workspace")
|
||||
def set_gruppe_workspace_endpoint(
|
||||
gruppe_id: str,
|
||||
payload: SetGruppeWorkspaceRequest,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
erfolg, fehlercode = set_gruppe_workspace(gruppe_id, payload.workspace_id)
|
||||
|
||||
if not erfolg:
|
||||
nachrichten = {
|
||||
"gruppe_not_found": "Die Gruppe wurde nicht gefunden.",
|
||||
"workspace_not_found": "Der Workspace wurde nicht gefunden.",
|
||||
"workspace_wrong_organization": "Dieser Workspace gehört einer anderen Organisation.",
|
||||
"workspace_conflict": "Mindestens ein Mitgliedsgerät ist bereits in einer anderen "
|
||||
"workspace-tragenden Gerätegruppe.",
|
||||
}
|
||||
return {
|
||||
"success": False,
|
||||
"error": fehlercode,
|
||||
"message": nachrichten.get(fehlercode, "Workspace konnte nicht zugewiesen werden."),
|
||||
}
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.delete("/api/v1/gruppen/{gruppe_id}/workspace")
|
||||
def clear_gruppe_workspace_endpoint(gruppe_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."}
|
||||
|
||||
if not clear_gruppe_workspace(gruppe_id):
|
||||
return {"success": False, "error": "gruppe_not_found", "message": "Die Gruppe wurde nicht gefunden."}
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.get("/api/v1/gruppen/{gruppe_id}/merkmale")
|
||||
def get_gruppe_merkmale(gruppe_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
@@ -4476,7 +4584,17 @@ def add_device_gruppe_endpoint(device_id: str, gruppe_id: str, authorization: st
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if not add_device_to_gruppe(device_id, gruppe_id):
|
||||
ergebnis = add_device_to_gruppe(device_id, gruppe_id)
|
||||
|
||||
if ergebnis == "conflict":
|
||||
return {
|
||||
"success": False,
|
||||
"error": "workspace_conflict",
|
||||
"message": "Das Gerät ist bereits in einer anderen Gerätegruppe mit eigenem Workspace. "
|
||||
"Ein Gerät kann nur in einer workspace-tragenden Gerätegruppe gleichzeitig sein.",
|
||||
}
|
||||
|
||||
if not ergebnis:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "invalid_membership",
|
||||
|
||||
Reference in new issue
Block a user