Initial commit
This commit is contained in:
186
app/routers/admin.py
Normal file
186
app/routers/admin.py
Normal 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"],
|
||||
}
|
||||
Reference in New Issue
Block a user