From b686d87c4b12d7859868846eb43727a3107d48af Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Fri, 11 Sep 2026 10:36:05 +0200 Subject: [PATCH] =?UTF-8?q?Workspace-Klon-Endpunkt=20f=C3=BCr=20"Diesen=20?= =?UTF-8?q?Standard=20=C3=BCbernehmen"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live gefundene Lücke (11.09.2026): das neue Besitz-Modell von routers/workspace.py bindet Webapps strikt an Eigentum - ein globaler, schreibgeschützter Standard-Workspace hat nie eigene Webapps, und eine Organisation ohne einen einzigen eigenen Workspace hatte dadurch gar keinen Weg mehr zu Webapps (Regression gegenüber dem alten, aber unsicheren Verhalten, das jede nutzende Organisation an geteilten Workspaces mitschreiben liess und damit ein Cross-Org-Leck riskierte). clone_workspace() kopiert die Merkmal-Zusammensetzung eines sichtbaren Workspace in einen neuen, garantiert der aufrufenden Organisation gehörenden Workspace - der Standard selbst bleibt für alle unangetastet. Neuer Service-Endpunkt POST /api/v1/workspaces/{id}/clone (Service-Token wie die übrigen Workspace-Endpunkte). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VLk93dhDMrkPXefVdknMV9 --- app.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/app.py b/app.py index b0511c7..c888897 100644 --- a/app.py +++ b/app.py @@ -205,6 +205,10 @@ class CreateWorkspaceRequest(BaseModel): organization_id: str | None = None +class CloneWorkspaceRequest(BaseModel): + organization_id: str + + class CreateWebappRequest(BaseModel): name: str url: str @@ -2598,6 +2602,51 @@ def create_workspace(key: str, name: str, description: str | None, organization_ return {"id": str(workspace_id), "key": key, "name": name} +def clone_workspace(source_workspace_id: str, organization_id: str): + """ + "Diesen Standard übernehmen" (GUI-Umbau Phase 3, Nachtrag 11.09.2026): + live gefundene Lücke - Webapps sind an Besitz gebunden (siehe + fetch_sichtbaren_workspace() in der Kundenplattform), ein global + schreibgeschützter Standard-Workspace hat also nie eigene Webapps, + und eine Organisation ohne einen einzigen eigenen Workspace hatte + dadurch gar keinen Weg mehr zu Webapps (anders als das alte, aber + unsichere Verhalten, wo jede Organisation, die einen geteilten + Workspace NUTZTE, dort auch Webapps eintragen konnte - das hätte + Webapps zwischen fremden Organisationen sichtbar gemacht, die + zufällig denselben globalen Standard verwenden). + + Löst das auf, ohne den schreibgeschützten Standard selbst anzutasten: + kopiert die Merkmal-Zusammensetzung in einen neuen, der Organisation + gehörenden Workspace - der Standard bleibt für alle unverändert, + die Kopie ist von Anfang an voll editierbar (inkl. eigener Webapps). + """ + + quelle = fetch_workspace_detail(source_workspace_id) + + if quelle is None: + return None + + with get_database_connection() as conn: + with conn.cursor() as cur: + cur.execute("SELECT key FROM workspaces WHERE organization_id = %s", (organization_id,)) + vergebene_keys = {row[0] for row in cur.fetchall()} + + basis_key = f"{quelle['key']}-eigene-kopie" + neuer_key = basis_key + zaehler = 2 + while neuer_key in vergebene_keys: + neuer_key = f"{basis_key}-{zaehler}" + zaehler += 1 + + neuer_workspace = create_workspace( + neuer_key, f"{quelle['name']} (eigene Kopie)", quelle["description"], organization_id + ) + + for merkmal in quelle["merkmale"]: + add_merkmal_to_workspace(neuer_workspace["id"], merkmal["id"]) + + return neuer_workspace + def fetch_workspace_detail(workspace_id: str): with get_database_connection() as conn: with conn.cursor() as cur: @@ -4177,6 +4226,21 @@ def create_workspace_endpoint(payload: CreateWorkspaceRequest, authorization: st } +@app.post("/api/v1/workspaces/{workspace_id}/clone") +def clone_workspace_endpoint( + workspace_id: str, payload: CloneWorkspaceRequest, authorization: str | None = Header(default=None) +): + if not require_service_token(authorization): + return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."} + + neuer_workspace = clone_workspace(workspace_id, payload.organization_id) + + if neuer_workspace is None: + return {"success": False, "error": "not_found", "message": "Workspace wurde nicht gefunden."} + + return {"success": True, "workspace": neuer_workspace} + + @app.get("/api/v1/workspaces/{workspace_id}") def get_workspace(workspace_id: str, authorization: str | None = Header(default=None)): if not require_service_token(authorization):