feat: Organisationen + Aktivierungscodes verwalten (Kundenplattform-Admin)

Neue Endpoints GET/POST /api/v1/organizations und GET/POST
/api/v1/organizations/{id}/activation-codes für den neuen Kunden-
Management-Bereich der Kundenplattform. Aktivierungscodes werden
serverseitig generiert (kein manuelles Ausdenken nötig).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Stallinger 2026-08-05 09:03:17 +02:00
parent 1e9020c5ba
commit e043233aea

115
app.py
View File

@ -61,6 +61,10 @@ class AuftragSelectRequest(BaseModel):
optionen: dict = {}
class CreateOrganizationRequest(BaseModel):
name: 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")
@ -599,6 +603,60 @@ def fetch_devices_for_organization(organization_id: str):
for device_id, hostname, fingerprint, last_checkin in cur.fetchall()
]
def fetch_organizations():
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT id, name, created_at FROM organizations ORDER BY name"
)
return [
{"id": str(org_id), "name": name, "created_at": created_at.isoformat()}
for org_id, name, created_at in cur.fetchall()
]
def create_organization(name: str):
organization_id = uuid4()
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"INSERT INTO organizations (id, name) VALUES (%s, %s)",
(organization_id, name),
)
return {"id": str(organization_id), "name": name}
def fetch_activation_codes(organization_id: str):
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT code, active, created_at
FROM activation_codes
WHERE organization_id = %s
ORDER BY created_at DESC
""",
(organization_id,),
)
return [
{"code": code, "active": active, "created_at": created_at.isoformat()}
for code, active, created_at in cur.fetchall()
]
def create_activation_code(organization_id: str):
code = "-".join(
secrets.token_hex(3).upper()[i:i + 3] for i in range(0, 6, 3)
)
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"INSERT INTO activation_codes (code, organization_id) VALUES (%s, %s)",
(code, organization_id),
)
return {"code": code, "active": True}
@app.get("/health")
def health():
return {
@ -904,6 +962,63 @@ def get_organization_devices(
return {"success": True, "devices": fetch_devices_for_organization(organization_id)}
@app.get("/api/v1/organizations")
def list_organizations(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, "organizations": fetch_organizations()}
@app.post("/api/v1/organizations")
def create_organization_endpoint(
payload: CreateOrganizationRequest,
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, "organization": create_organization(payload.name)}
@app.get("/api/v1/organizations/{organization_id}/activation-codes")
def list_activation_codes(
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, "activation_codes": fetch_activation_codes(organization_id)}
@app.post("/api/v1/organizations/{organization_id}/activation-codes")
def create_activation_code_endpoint(
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, "activation_code": create_activation_code(organization_id)}
@app.get("/api/v1/devices/{device_id}/auftragskatalog")
def get_device_auftragskatalog(
device_id: str,