Initial commit

This commit is contained in:
root
2026-04-29 08:17:35 +00:00
commit ef55253cbd
49 changed files with 3073 additions and 0 deletions

186
app/routers/admin.py Normal file
View File

@@ -0,0 +1,186 @@
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"],
}