feat: Merkmale/Kategorien/Workspaces verwalten (Kundenplattform-Admin)
CRUD-Endpoints für den Admin-Bereich "Verwaltung -> Technik/Datenbank": Merkmale (inkl. Kategorie-Zuordnung, im_auftragskatalog-Flag), Kategorien, Workspaces samt Zusammensetzung aus Merkmalen (workspace_merkmale hinzufügen/entfernen - Kernbaustein des später geplanten Workspace-Creators). Backends/Blueprints nur lesend, da deren Änderung echten Ansible-Rollen-Code voraussetzt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
d7956bf8f7
commit
34f9c9b2ef
361
app.py
361
app.py
@ -65,6 +65,33 @@ class CreateOrganizationRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class CreateMerkmalRequest(BaseModel):
|
||||
key: str
|
||||
name: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class UpdateMerkmalRequest(BaseModel):
|
||||
name: str
|
||||
description: str | None = None
|
||||
kategorie_id: str | None = None
|
||||
im_auftragskatalog: bool = False
|
||||
|
||||
|
||||
class CreateKategorieRequest(BaseModel):
|
||||
key: str
|
||||
name: str
|
||||
description: str | None = None
|
||||
sort_order: int = 0
|
||||
|
||||
|
||||
class CreateWorkspaceRequest(BaseModel):
|
||||
key: str
|
||||
name: str
|
||||
description: str | None = None
|
||||
organization_id: str | None = None
|
||||
|
||||
|
||||
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")
|
||||
@ -692,6 +719,220 @@ def create_activation_code(organization_id: str):
|
||||
|
||||
return {"code": code, "active": True}
|
||||
|
||||
def fetch_merkmale():
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.id, m.key, m.name, m.description, m.im_auftragskatalog,
|
||||
m.kategorie_id, k.name
|
||||
FROM merkmale m
|
||||
LEFT JOIN kategorien k ON k.id = m.kategorie_id
|
||||
ORDER BY m.key
|
||||
"""
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": str(mid),
|
||||
"key": key,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"im_auftragskatalog": im_auftragskatalog,
|
||||
"kategorie_id": str(kategorie_id) if kategorie_id else None,
|
||||
"kategorie_name": kategorie_name,
|
||||
}
|
||||
for (
|
||||
mid, key, name, description, im_auftragskatalog,
|
||||
kategorie_id, kategorie_name,
|
||||
) in cur.fetchall()
|
||||
]
|
||||
|
||||
def create_merkmal(key: str, name: str, description: str | None):
|
||||
merkmal_id = uuid4()
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO merkmale (id, key, name, description) VALUES (%s, %s, %s, %s)",
|
||||
(merkmal_id, key, name, description),
|
||||
)
|
||||
|
||||
return {"id": str(merkmal_id), "key": key, "name": name}
|
||||
|
||||
def update_merkmal(merkmal_id: str, name: str, description: str | None, kategorie_id: str | None, im_auftragskatalog: bool):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE merkmale
|
||||
SET name = %s, description = %s, kategorie_id = %s, im_auftragskatalog = %s
|
||||
WHERE id = %s
|
||||
RETURNING id
|
||||
""",
|
||||
(name, description, kategorie_id, im_auftragskatalog, merkmal_id),
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
|
||||
def fetch_kategorien():
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id, key, name, description, sort_order FROM kategorien ORDER BY sort_order, name"
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": str(kid),
|
||||
"key": key,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"sort_order": sort_order,
|
||||
}
|
||||
for kid, key, name, description, sort_order in cur.fetchall()
|
||||
]
|
||||
|
||||
def create_kategorie(key: str, name: str, description: str | None, sort_order: int):
|
||||
kategorie_id = uuid4()
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO kategorien (id, key, name, description, sort_order)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
""",
|
||||
(kategorie_id, key, name, description, sort_order),
|
||||
)
|
||||
|
||||
return {"id": str(kategorie_id), "key": key, "name": name}
|
||||
|
||||
def fetch_workspaces():
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT w.id, w.key, w.name, w.description, w.organization_id, o.name
|
||||
FROM workspaces w
|
||||
LEFT JOIN organizations o ON o.id = w.organization_id
|
||||
ORDER BY o.name NULLS FIRST, w.name
|
||||
"""
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": str(wid),
|
||||
"key": key,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"organization_id": str(organization_id) if organization_id else None,
|
||||
"organization_name": organization_name,
|
||||
}
|
||||
for (
|
||||
wid, key, name, description, organization_id, organization_name,
|
||||
) in cur.fetchall()
|
||||
]
|
||||
|
||||
def create_workspace(key: str, name: str, description: str | None, organization_id: str | None):
|
||||
workspace_id = uuid4()
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO workspaces (id, organization_id, key, name, description)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
""",
|
||||
(workspace_id, organization_id, key, name, description),
|
||||
)
|
||||
|
||||
return {"id": str(workspace_id), "key": key, "name": name}
|
||||
|
||||
def fetch_workspace_detail(workspace_id: str):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT w.id, w.key, w.name, w.description, w.organization_id, o.name
|
||||
FROM workspaces w
|
||||
LEFT JOIN organizations o ON o.id = w.organization_id
|
||||
WHERE w.id = %s
|
||||
""",
|
||||
(workspace_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.id, m.key, m.name
|
||||
FROM workspace_merkmale wm
|
||||
JOIN merkmale m ON m.id = wm.merkmal_id
|
||||
WHERE wm.workspace_id = %s
|
||||
ORDER BY m.key
|
||||
""",
|
||||
(workspace_id,),
|
||||
)
|
||||
merkmale = [
|
||||
{"id": str(mid), "key": key, "name": name}
|
||||
for mid, key, name in cur.fetchall()
|
||||
]
|
||||
|
||||
return {
|
||||
"id": str(row[0]),
|
||||
"key": row[1],
|
||||
"name": row[2],
|
||||
"description": row[3],
|
||||
"organization_id": str(row[4]) if row[4] else None,
|
||||
"organization_name": row[5],
|
||||
"merkmale": merkmale,
|
||||
}
|
||||
|
||||
def add_merkmal_to_workspace(workspace_id: str, merkmal_id: str):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO workspace_merkmale (workspace_id, merkmal_id)
|
||||
VALUES (%s, %s)
|
||||
ON CONFLICT DO NOTHING
|
||||
""",
|
||||
(workspace_id, merkmal_id),
|
||||
)
|
||||
|
||||
def remove_merkmal_from_workspace(workspace_id: str, merkmal_id: str):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM workspace_merkmale WHERE workspace_id = %s AND merkmal_id = %s",
|
||||
(workspace_id, merkmal_id),
|
||||
)
|
||||
|
||||
def fetch_backends():
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT id, key, name, installer_type, version FROM backends ORDER BY key")
|
||||
return [
|
||||
{"id": str(bid), "key": key, "name": name, "installer_type": installer_type, "version": version}
|
||||
for bid, key, name, installer_type, version in cur.fetchall()
|
||||
]
|
||||
|
||||
def fetch_blueprints():
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.key, b.key, bp.ansible_role
|
||||
FROM blueprints bp
|
||||
JOIN merkmale m ON m.id = bp.merkmal_id
|
||||
JOIN backends b ON b.id = bp.backend_id
|
||||
ORDER BY m.key, b.key
|
||||
"""
|
||||
)
|
||||
return [
|
||||
{"merkmal": merkmal_key, "backend": backend_key, "ansible_role": ansible_role}
|
||||
for merkmal_key, backend_key, ansible_role in cur.fetchall()
|
||||
]
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {
|
||||
@ -1066,6 +1307,126 @@ def create_activation_code_endpoint(
|
||||
return {"success": True, "activation_code": create_activation_code(organization_id)}
|
||||
|
||||
|
||||
@app.get("/api/v1/merkmale")
|
||||
def list_merkmale(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, "merkmale": fetch_merkmale()}
|
||||
|
||||
|
||||
@app.post("/api/v1/merkmale")
|
||||
def create_merkmal_endpoint(payload: CreateMerkmalRequest, 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, "merkmal": create_merkmal(payload.key, payload.name, payload.description)}
|
||||
|
||||
|
||||
@app.patch("/api/v1/merkmale/{merkmal_id}")
|
||||
def update_merkmal_endpoint(
|
||||
merkmal_id: str, payload: UpdateMerkmalRequest, authorization: str | None = Header(default=None)
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
updated = update_merkmal(
|
||||
merkmal_id, payload.name, payload.description, payload.kategorie_id, payload.im_auftragskatalog
|
||||
)
|
||||
|
||||
if not updated:
|
||||
return {"success": False, "error": "not_found", "message": "Merkmal wurde nicht gefunden."}
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.get("/api/v1/kategorien")
|
||||
def list_kategorien(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, "kategorien": fetch_kategorien()}
|
||||
|
||||
|
||||
@app.post("/api/v1/kategorien")
|
||||
def create_kategorie_endpoint(payload: CreateKategorieRequest, 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,
|
||||
"kategorie": create_kategorie(payload.key, payload.name, payload.description, payload.sort_order),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/v1/workspaces")
|
||||
def list_workspaces(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_workspaces()}
|
||||
|
||||
|
||||
@app.post("/api/v1/workspaces")
|
||||
def create_workspace_endpoint(payload: CreateWorkspaceRequest, 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,
|
||||
"workspace": create_workspace(payload.key, payload.name, payload.description, payload.organization_id),
|
||||
}
|
||||
|
||||
|
||||
@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):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
workspace = fetch_workspace_detail(workspace_id)
|
||||
|
||||
if workspace is None:
|
||||
return {"success": False, "error": "not_found", "message": "Workspace wurde nicht gefunden."}
|
||||
|
||||
return {"success": True, "workspace": workspace}
|
||||
|
||||
|
||||
@app.post("/api/v1/workspaces/{workspace_id}/merkmale/{merkmal_id}")
|
||||
def add_workspace_merkmal(workspace_id: str, merkmal_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."}
|
||||
|
||||
add_merkmal_to_workspace(workspace_id, merkmal_id)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.delete("/api/v1/workspaces/{workspace_id}/merkmale/{merkmal_id}")
|
||||
def remove_workspace_merkmal(workspace_id: str, merkmal_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."}
|
||||
|
||||
remove_merkmal_from_workspace(workspace_id, merkmal_id)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.get("/api/v1/backends")
|
||||
def list_backends(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, "backends": fetch_backends()}
|
||||
|
||||
|
||||
@app.get("/api/v1/blueprints")
|
||||
def list_blueprints(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, "blueprints": fetch_blueprints()}
|
||||
|
||||
|
||||
@app.get("/api/v1/devices/{device_id}/auftragskatalog")
|
||||
def get_device_auftragskatalog(
|
||||
device_id: str,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user