187 lines
5.0 KiB
Python
187 lines
5.0 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from app.auth import verify_api_key
|
|
from app.database import get_db
|
|
import httpx
|
|
import os
|
|
|
|
router = APIRouter()
|
|
|
|
LITELLM_URL = os.getenv("LITELLM_PROXY_URL", "http://litellm:4000")
|
|
MASTER_KEY = os.getenv("LITELLM_MASTER_KEY")
|
|
ADMIN_IDS = os.getenv("ADMIN_USER_IDS", "").split(",")
|
|
|
|
|
|
# --- Admin-Check ---
|
|
async def require_admin(user: dict = Depends(verify_api_key)):
|
|
if user["user_id"] not in ADMIN_IDS:
|
|
raise HTTPException(403, "Admin-Zugriff erforderlich")
|
|
return user
|
|
|
|
|
|
# --- Stats ---
|
|
@router.get("/stats")
|
|
async def get_stats(
|
|
admin=Depends(require_admin),
|
|
db=Depends(get_db)
|
|
):
|
|
stats = await db.fetchrow(
|
|
"""SELECT
|
|
(SELECT COUNT(*) FROM vector_stores) AS total_stores,
|
|
(SELECT COUNT(*) FROM documents) AS total_documents,
|
|
(SELECT COUNT(DISTINCT owner_user_id)
|
|
FROM vector_stores) AS total_users,
|
|
(SELECT COUNT(*) FROM store_permissions) AS total_permissions"""
|
|
)
|
|
return dict(stats)
|
|
|
|
|
|
# --- User Endpoints ---
|
|
@router.get("/users")
|
|
async def list_users(
|
|
admin=Depends(require_admin),
|
|
db=Depends(get_db)
|
|
):
|
|
rows = await db.fetch(
|
|
"""SELECT
|
|
owner_user_id AS user_id,
|
|
COUNT(id) AS store_count,
|
|
MAX(created_at) AS last_activity
|
|
FROM vector_stores
|
|
GROUP BY owner_user_id
|
|
ORDER BY last_activity DESC"""
|
|
)
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
@router.get("/users/{user_id}/stores")
|
|
async def get_user_stores(
|
|
user_id: str,
|
|
admin=Depends(require_admin),
|
|
db=Depends(get_db)
|
|
):
|
|
rows = await db.fetch(
|
|
"""SELECT
|
|
vs.id,
|
|
vs.name,
|
|
vs.created_at,
|
|
COUNT(d.id) AS document_count
|
|
FROM vector_stores vs
|
|
LEFT JOIN documents d ON d.store_id = vs.id
|
|
WHERE vs.owner_user_id = $1
|
|
GROUP BY vs.id, vs.name, vs.created_at""",
|
|
user_id
|
|
)
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
@router.delete("/users/{user_id}/stores/{store_id}")
|
|
async def admin_delete_store(
|
|
user_id: str,
|
|
store_id: str,
|
|
admin=Depends(require_admin),
|
|
db=Depends(get_db)
|
|
):
|
|
deleted = await db.fetchval(
|
|
"""DELETE FROM vector_stores
|
|
WHERE id = $1 AND owner_user_id = $2
|
|
RETURNING id""",
|
|
store_id, user_id
|
|
)
|
|
if not deleted:
|
|
raise HTTPException(404, "Store nicht gefunden")
|
|
return {"deleted": str(deleted)}
|
|
|
|
|
|
# --- Permission Endpoints ---
|
|
@router.get("/stores/{store_id}/permissions")
|
|
async def get_permissions(
|
|
store_id: str,
|
|
admin=Depends(require_admin),
|
|
db=Depends(get_db)
|
|
):
|
|
rows = await db.fetch(
|
|
"""SELECT user_id, permission, created_at
|
|
FROM store_permissions
|
|
WHERE store_id = $1""",
|
|
store_id
|
|
)
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
@router.post("/stores/{store_id}/permissions")
|
|
async def grant_permission(
|
|
store_id: str,
|
|
user_id: str,
|
|
permission: str = "read",
|
|
admin=Depends(require_admin),
|
|
db=Depends(get_db)
|
|
):
|
|
if permission not in ("read", "write", "admin"):
|
|
raise HTTPException(400, "Ungültige Permission: read, write oder admin")
|
|
|
|
await db.execute(
|
|
"""INSERT INTO store_permissions (store_id, user_id, permission)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (store_id, user_id)
|
|
DO UPDATE SET permission = $3""",
|
|
store_id, user_id, permission
|
|
)
|
|
return {"granted": permission, "user_id": user_id}
|
|
|
|
|
|
@router.delete("/stores/{store_id}/permissions/{user_id}")
|
|
async def revoke_permission(
|
|
store_id: str,
|
|
user_id: str,
|
|
admin=Depends(require_admin),
|
|
db=Depends(get_db)
|
|
):
|
|
await db.execute(
|
|
"DELETE FROM store_permissions WHERE store_id=$1 AND user_id=$2",
|
|
store_id, user_id
|
|
)
|
|
return {"revoked": user_id}
|
|
|
|
|
|
# --- Key Management ---
|
|
@router.post("/users/{user_id}/rotate-key")
|
|
async def rotate_key(
|
|
user_id: str,
|
|
admin=Depends(require_admin),
|
|
db=Depends(get_db)
|
|
):
|
|
async with httpx.AsyncClient() as client:
|
|
resp = await client.post(
|
|
f"{LITELLM_URL}/key/generate",
|
|
headers={"Authorization": f"Bearer {MASTER_KEY}"},
|
|
json={
|
|
"user_id": user_id,
|
|
"key_alias": f"{user_id}-rotated"
|
|
}
|
|
)
|
|
if resp.status_code != 200:
|
|
raise HTTPException(500, "Key-Rotation fehlgeschlagen")
|
|
|
|
store_count = await db.fetchval(
|
|
"SELECT COUNT(*) FROM vector_stores WHERE owner_user_id=$1",
|
|
user_id
|
|
)
|
|
return {
|
|
"new_key": resp.json()["key"],
|
|
"user_id": user_id,
|
|
"stores_preserved": store_count
|
|
}
|
|
|
|
@router.get("/verify")
|
|
async def verify_admin(
|
|
admin=Depends(require_admin),
|
|
):
|
|
"""
|
|
Prüft ob der API Key Admin-Rechte hat.
|
|
Gibt 200 zurück wenn Admin, 403 wenn nicht.
|
|
"""
|
|
return {
|
|
"admin": True,
|
|
"user_id": admin["user_id"],
|
|
}
|