diff --git a/app.py b/app.py index 4155690..34e0e10 100644 --- a/app.py +++ b/app.py @@ -1645,6 +1645,11 @@ def fetch_auftragskatalog_state(device_id: str): aktiv = workspace_default optionen = {} + if key == ORG_WLAN_MERKMAL and merkmal_id not in overrides: + org_wlan = org_wlan_fuer_geraet(device_id) + if org_wlan: + aktiv, optionen = True, org_wlan + optionen = decrypt_sensitive_optionen(optionen) if key == VEYON_MERKMAL and aktiv: optionen = veyon_optionen_anreichern(device_id, merkmal_id, optionen) @@ -4173,6 +4178,87 @@ def create_organization(name: str, kundengruppe_id: str | None = None): return {"id": str(organization_id), "name": name, "standard_workspace": standard} +# WLAN der Organisation (Einstieg vereinfachen, Schritt 2, Migration 0074). +# Gespeichert in der Form der Merkmal-Optionen der Rolle org-wlan; ein am +# Merkmal selbst (OE/Gruppe) gesetzter Wert hat Vorrang. +ORG_WLAN_MERKMAL = "org-wlan" + + +def org_wlan_pruefen(ssid: str, passwort: str, offen: bool) -> str | None: + if not ssid: + return None + if len(ssid.encode("utf-8")) > 32 or "\n" in ssid or "\r" in ssid: + return "Der WLAN-Name (SSID) darf höchstens 32 Bytes lang sein." + if offen: + return None + if not passwort: + return "Bitte das WLAN-Passwort eingeben oder „Offenes WLAN“ wählen." + if "\n" in passwort or "\r" in passwort or not (8 <= len(passwort) <= 63 or re.fullmatch(r"[0-9A-Fa-f]{64}", passwort)): + return "Das WLAN-Passwort muss 8 bis 63 Zeichen lang sein." + return None + + +def fetch_org_wlan(organization_id: str, mit_passwort: bool = False) -> dict | None: + """{"ssid", "offen", "hat_passwort"[, "passwort"]} oder None ohne WLAN.""" + + with get_database_connection() as conn: + with conn.cursor() as cur: + cur.execute("SELECT org_wlan FROM organizations WHERE id = %s", (organization_id,)) + row = cur.fetchone() + if not row or not row[0] or not row[0].get("wlan_ssid"): + return None + gespeichert = row[0] + ergebnis = { + "ssid": gespeichert["wlan_ssid"], + "offen": not gespeichert.get("wlan_passwort"), + "hat_passwort": bool(gespeichert.get("wlan_passwort")), + } + if mit_passwort: + ergebnis["passwort"] = decrypt_sensitive_optionen(gespeichert).get("wlan_passwort") or "" + return ergebnis + + +def set_org_wlan(organization_id: str, ssid: str, passwort: str, offen: bool) -> str | None: + """Speichert das WLAN der Organisation; leere SSID entfernt es. Ein leeres + Passwort bei gleicher SSID behält das gespeicherte. Fehlermeldung oder None.""" + + ssid = (ssid or "").strip() + with get_database_connection() as conn: + with conn.cursor() as cur: + cur.execute("SELECT org_wlan FROM organizations WHERE id = %s FOR UPDATE", (organization_id,)) + row = cur.fetchone() + if row is None: + return "Die Organisation wurde nicht gefunden." + bisher = row[0] or {} + if ssid and not offen and not passwort and bisher.get("wlan_ssid") == ssid and bisher.get("wlan_passwort"): + neu = bisher + else: + fehler = org_wlan_pruefen(ssid, passwort, offen) + if fehler: + return fehler + neu = encrypt_sensitive_optionen({"wlan_ssid": ssid, "wlan_passwort": "" if offen else passwort}) if ssid else None + cur.execute( + "UPDATE organizations SET org_wlan = %s, updated_at = now() WHERE id = %s", + (Jsonb(neu) if neu else None, organization_id), + ) + return None + + +def org_wlan_fuer_geraet(device_id: str) -> dict | None: + """Optionen für die Rolle org-wlan aus dem WLAN der Organisation des Geräts.""" + + with get_database_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "SELECT o.org_wlan FROM devices d JOIN organizations o ON o.id = d.organization_id WHERE d.id = %s", + (device_id,), + ) + row = cur.fetchone() + if not row or not row[0] or not row[0].get("wlan_ssid"): + return None + return dict(row[0]) + + def standard_ausstattung_anlegen(cur, organization_id, standard_oe_id, kundengruppe_id) -> str | None: """ Einstieg vereinfachen, Schritt 1 (25.10.2026): Hat die Art der @@ -8021,6 +8107,29 @@ def get_organization_endpoint(organization_id: str, authorization: str | None = return {"success": True, "organization": organization} +class OrgWlanRequest(BaseModel): + ssid: str = "" + passwort: str = "" + offen: bool = False + + +@app.get("/api/v1/organizations/{organization_id}/org-wlan") +def get_org_wlan_endpoint(organization_id: str, mit_passwort: bool = False, 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, "org_wlan": fetch_org_wlan(organization_id, mit_passwort)} + + +@app.put("/api/v1/organizations/{organization_id}/org-wlan") +def put_org_wlan_endpoint(organization_id: str, payload: OrgWlanRequest, authorization: str | None = Header(default=None)): + if not require_service_token(authorization): + return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."} + fehler = set_org_wlan(organization_id, payload.ssid, payload.passwort, payload.offen) + if fehler: + return {"success": False, "error": "ungueltig", "message": fehler} + return {"success": True, "org_wlan": fetch_org_wlan(organization_id)} + + class SetAusrollstufeRequest(BaseModel): ausrollstufe: str diff --git a/migrations/0074_org_wlan.sql b/migrations/0074_org_wlan.sql new file mode 100644 index 0000000..66d4294 --- /dev/null +++ b/migrations/0074_org_wlan.sql @@ -0,0 +1,16 @@ +BEGIN; + +-- Einstieg vereinfachen, Schritt 2 (platform-docs roadmap/einstieg- +-- vereinfachen.md, 25.10.2026): Das WLAN der Organisation ist eine +-- Einstellung der Organisation, nicht mehr nur das WLAN vom +-- Installationsmedium. Alle Geräte bekommen es über die Rolle org-wlan, +-- auch per Kabel installierte (Befund Liebherr-83, 14.10.2026). +-- +-- Form wie die Merkmal-Optionen der Rolle: {"wlan_ssid", "wlan_passwort"} +-- mit "enc:"-verschlüsseltem Passwort (encrypt_sensitive_optionen); NULL = +-- kein WLAN der Organisation. Ein am Merkmal "Org-WLAN" (OE/Gruppe) +-- gesetzter Wert hat weiter Vorrang. + +ALTER TABLE organizations ADD COLUMN org_wlan JSONB; + +COMMIT;