Der Monitor löst den KI-Weg als Lage vor Organisation vor Servervorgabe auf und liest dafür das Org-Setting ai_backend. Geschrieben werden konnte es bisher nur direkt in der Datenbank, eine Oberfläche gab es nicht. Für einen Kunden, der die DSGVO-Fassung will, ist das der praktisch wichtigste Weg, denn der stellt einmal zentral um und nicht Lage für Lage. Das Feld steht jetzt dort, wo die Pipeline-Sprache schon steht, also als eigene Spalte in der Lizenztabelle der aktiven Lizenz, und zusätzlich im Anlege-Dialog für neue Organisationen. Drei Werte. Leer bedeutet Servervorgabe, cli den heutigen Weg über Anthropic, bedrock die EU-Fassung über AWS Bedrock in Frankfurt mit Recherche über den europäischen Suchindex staan. Der Leerstring ist dabei ein gültiger Wert und keine fehlende Angabe. Der Router prüft deshalb auf None statt auf Wahrheit, sonst ließe sich eine gesetzte Vorgabe nie wieder auf die Servervorgabe zurücknehmen. 26 Prüfungen, ohne Datenbank und ohne Netz. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
185 Zeilen
6.9 KiB
Python
185 Zeilen
6.9 KiB
Python
"""Organisations-CRUD."""
|
|
from datetime import datetime, timezone
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|
from models import OrgCreate, OrgUpdate, OrgResponse
|
|
from auth import get_current_admin
|
|
from database import db_dependency
|
|
from audit import log_action, get_client_ip, row_to_dict
|
|
import aiosqlite
|
|
|
|
router = APIRouter(prefix="/api/orgs", tags=["organizations"])
|
|
|
|
|
|
async def _enrich_org(db: aiosqlite.Connection, row: aiosqlite.Row) -> dict:
|
|
org = dict(row)
|
|
cursor = await db.execute(
|
|
"SELECT COUNT(*) as cnt FROM users WHERE organization_id = ? AND is_active = 1",
|
|
(org["id"],),
|
|
)
|
|
org["user_count"] = (await cursor.fetchone())["cnt"]
|
|
|
|
cursor = await db.execute(
|
|
"SELECT license_type, status FROM licenses WHERE organization_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 1",
|
|
(org["id"],),
|
|
)
|
|
lic = await cursor.fetchone()
|
|
org["license_status"] = lic["status"] if lic else "none"
|
|
org["license_type"] = lic["license_type"] if lic else ""
|
|
|
|
# output_language aus organization_settings (Default 'de')
|
|
cursor = await db.execute(
|
|
"SELECT value FROM organization_settings WHERE organization_id = ? AND key = 'output_language'",
|
|
(org["id"],),
|
|
)
|
|
lang_row = await cursor.fetchone()
|
|
org["output_language"] = lang_row["value"] if lang_row else "de"
|
|
|
|
# ai_backend aus organization_settings (leer = Servervorgabe)
|
|
cursor = await db.execute(
|
|
"SELECT value FROM organization_settings WHERE organization_id = ? AND key = 'ai_backend'",
|
|
(org["id"],),
|
|
)
|
|
ai_row = await cursor.fetchone()
|
|
org["ai_backend"] = (ai_row["value"] if ai_row else "") or ""
|
|
|
|
return org
|
|
|
|
|
|
@router.get("", response_model=list[OrgResponse])
|
|
async def list_organizations(
|
|
admin: dict = Depends(get_current_admin),
|
|
db: aiosqlite.Connection = Depends(db_dependency),
|
|
):
|
|
cursor = await db.execute("SELECT * FROM organizations ORDER BY created_at DESC")
|
|
rows = await cursor.fetchall()
|
|
return [await _enrich_org(db, row) for row in rows]
|
|
|
|
|
|
@router.post("", response_model=OrgResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_organization(
|
|
data: OrgCreate,
|
|
request: Request,
|
|
admin: dict = Depends(get_current_admin),
|
|
db: aiosqlite.Connection = Depends(db_dependency),
|
|
):
|
|
cursor = await db.execute("SELECT id FROM organizations WHERE slug = ?", (data.slug,))
|
|
if await cursor.fetchone():
|
|
raise HTTPException(status_code=400, detail="Slug bereits vergeben")
|
|
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
cursor = await db.execute(
|
|
"INSERT INTO organizations (name, slug, is_active, created_at, updated_at) VALUES (?, ?, 1, ?, ?)",
|
|
(data.name, data.slug, now, now),
|
|
)
|
|
org_id = cursor.lastrowid
|
|
|
|
# Optional direkt eine Lizenz mit Credits-Kontingent anlegen. Beide Inserts
|
|
# liegen VOR dem Commit, schlaegt der Lizenz-Teil fehl (z.B. fehlende
|
|
# Spalten), entsteht keine halb angelegte Organisation.
|
|
lic_id = None
|
|
revoked_lics: list = []
|
|
if data.license is not None:
|
|
from routers.licenses import insert_license_row
|
|
lic_id, revoked_lics = await insert_license_row(db, org_id, data.license)
|
|
|
|
await db.commit()
|
|
|
|
# output_language als organization_settings-Eintrag persistieren
|
|
from shared.services.org_settings import set_org_setting
|
|
await set_org_setting(db, org_id, "output_language", data.output_language)
|
|
await set_org_setting(db, org_id, "ai_backend", data.ai_backend or "")
|
|
|
|
cursor = await db.execute("SELECT * FROM organizations WHERE id = ?", (org_id,))
|
|
new_row_obj = await cursor.fetchone()
|
|
await log_action(
|
|
db, admin, get_client_ip(request),
|
|
action="create", resource_type="organization", resource_id=org_id,
|
|
after=dict(new_row_obj),
|
|
)
|
|
if lic_id is not None:
|
|
from routers.licenses import audit_license_creation
|
|
await audit_license_creation(
|
|
db, admin, get_client_ip(request), lic_id, revoked_lics
|
|
)
|
|
return await _enrich_org(db, new_row_obj)
|
|
|
|
|
|
@router.get("/{org_id}", response_model=OrgResponse)
|
|
async def get_organization(
|
|
org_id: int,
|
|
admin: dict = Depends(get_current_admin),
|
|
db: aiosqlite.Connection = Depends(db_dependency),
|
|
):
|
|
cursor = await db.execute("SELECT * FROM organizations WHERE id = ?", (org_id,))
|
|
row = await cursor.fetchone()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Organisation nicht gefunden")
|
|
return await _enrich_org(db, row)
|
|
|
|
|
|
@router.put("/{org_id}", response_model=OrgResponse)
|
|
async def update_organization(
|
|
org_id: int,
|
|
data: OrgUpdate,
|
|
request: Request,
|
|
admin: dict = Depends(get_current_admin),
|
|
db: aiosqlite.Connection = Depends(db_dependency),
|
|
):
|
|
before = await row_to_dict(db, "organizations", org_id)
|
|
if not before:
|
|
raise HTTPException(status_code=404, detail="Organisation nicht gefunden")
|
|
|
|
updates = {}
|
|
if data.name is not None:
|
|
updates["name"] = data.name
|
|
if data.is_active is not None:
|
|
updates["is_active"] = 1 if data.is_active else 0
|
|
|
|
if updates:
|
|
updates["updated_at"] = datetime.now(timezone.utc).isoformat()
|
|
set_clause = ", ".join(f"{k} = ?" for k in updates)
|
|
values = list(updates.values()) + [org_id]
|
|
await db.execute(f"UPDATE organizations SET {set_clause} WHERE id = ?", values)
|
|
await db.commit()
|
|
|
|
# output_language separat ueber organization_settings setzen
|
|
if data.output_language is not None:
|
|
from shared.services.org_settings import set_org_setting
|
|
await set_org_setting(db, org_id, "output_language", data.output_language)
|
|
|
|
# KI-Weg. Der Leerstring ist ein gueltiger Wert und bedeutet
|
|
# "Servervorgabe", deshalb hier die Pruefung auf None statt auf Wahrheit.
|
|
if data.ai_backend is not None:
|
|
from shared.services.org_settings import set_org_setting
|
|
await set_org_setting(db, org_id, "ai_backend", data.ai_backend)
|
|
|
|
after = await row_to_dict(db, "organizations", org_id)
|
|
await log_action(
|
|
db, admin, get_client_ip(request),
|
|
action="update", resource_type="organization", resource_id=org_id,
|
|
before=before, after=after,
|
|
)
|
|
|
|
cursor = await db.execute("SELECT * FROM organizations WHERE id = ?", (org_id,))
|
|
return await _enrich_org(db, await cursor.fetchone())
|
|
|
|
|
|
@router.delete("/{org_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_organization(
|
|
org_id: int,
|
|
request: Request,
|
|
admin: dict = Depends(get_current_admin),
|
|
db: aiosqlite.Connection = Depends(db_dependency),
|
|
):
|
|
before = await row_to_dict(db, "organizations", org_id)
|
|
if not before:
|
|
raise HTTPException(status_code=404, detail="Organisation nicht gefunden")
|
|
|
|
await db.execute("DELETE FROM organizations WHERE id = ?", (org_id,))
|
|
await db.commit()
|
|
await log_action(
|
|
db, admin, get_client_ip(request),
|
|
action="delete", resource_type="organization", resource_id=org_id,
|
|
before=before,
|
|
)
|