Files
aza/APP/backup 24.2.26/admin_routes.py

205 lines
6.8 KiB
Python
Raw Normal View History

2026-03-25 14:14:07 +01:00
"""
AZA / MedWork - Admin Routes (FastAPI)
Separiert die Admin-Endpunkte aus license_server.py:
- /admin/set_plan
- /admin/revoke_token
- /admin/set_status
- /admin/audit/list
Alle Endpunkte sind per AZA_ADMIN_KEY geschützt.
"""
from __future__ import annotations
from typing import Optional, Callable, Any
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
# -----------------------------
# Models
# -----------------------------
class AdminSetPlanRequest(BaseModel):
admin_key: str = Field(..., min_length=1, max_length=512)
email: str = Field(..., min_length=3, max_length=255)
plan: str = Field(..., min_length=1, max_length=64)
class AdminSetPlanResponse(BaseModel):
ok: bool
email: str
plan: str
message: str
class AdminRevokeTokenRequest(BaseModel):
admin_key: str = Field(..., min_length=1, max_length=512)
token: str = Field(..., min_length=1, max_length=4096)
class AdminRevokeTokenResponse(BaseModel):
ok: bool
token: str
message: str
class AdminSetStatusRequest(BaseModel):
admin_key: str = Field(..., min_length=1, max_length=512)
email: str = Field(..., min_length=3, max_length=255)
status: str = Field(..., min_length=1, max_length=64)
class AdminSetStatusResponse(BaseModel):
ok: bool
email: str
status: str
message: str
class AdminAuditListRequest(BaseModel):
admin_key: str = Field(..., min_length=1, max_length=512)
limit: int = Field(50, ge=1, le=500)
class AdminAuditItem(BaseModel):
id: int
action: str
email: Optional[str] = None
token: Optional[str] = None
old_value: Optional[str] = None
new_value: Optional[str] = None
created_at: str
class AdminAuditListResponse(BaseModel):
ok: bool
items: list[AdminAuditItem]
# -----------------------------
# Router factory
# -----------------------------
def build_admin_router(
*,
get_db: Callable[[], Any],
admin_key: str,
log_admin_action: Callable[..., None],
) -> APIRouter:
"""
get_db: function returning sqlite3.Connection (row_factory set)
admin_key: AZA_ADMIN_KEY (string)
log_admin_action: function(conn, action, email?, token?, old_value?, new_value?)
"""
router = APIRouter(prefix="/admin", tags=["admin"])
def _require_admin_key(provided: str) -> None:
if not admin_key:
raise HTTPException(status_code=503, detail="Admin not configured (AZA_ADMIN_KEY missing)")
if (provided or "").strip() != admin_key:
raise HTTPException(status_code=401, detail="Invalid admin key")
@router.post("/set_plan", response_model=AdminSetPlanResponse)
def admin_set_plan(req: AdminSetPlanRequest):
_require_admin_key(req.admin_key)
email = req.email.strip().lower()
plan = req.plan.strip().lower()
if not email or "@" not in email:
raise HTTPException(status_code=400, detail="Invalid email")
if not plan:
raise HTTPException(status_code=400, detail="Invalid plan")
conn = get_db()
try:
row = conn.execute("SELECT id, plan FROM users WHERE email = ?", (email,)).fetchone()
if not row:
raise HTTPException(status_code=404, detail="User not found")
old_plan = str(row["plan"]) if row["plan"] else ""
conn.execute("UPDATE users SET plan = ? WHERE email = ?", (plan, email))
log_admin_action(conn, action="set_plan", email=email, old_value=old_plan, new_value=plan)
conn.commit()
return AdminSetPlanResponse(ok=True, email=email, plan=plan, message="Plan updated")
finally:
conn.close()
@router.post("/revoke_token", response_model=AdminRevokeTokenResponse)
def admin_revoke_token(req: AdminRevokeTokenRequest):
_require_admin_key(req.admin_key)
token = req.token.strip()
if not token:
raise HTTPException(status_code=400, detail="Invalid token")
conn = get_db()
try:
row = conn.execute("SELECT token FROM tokens WHERE token = ?", (token,)).fetchone()
if not row:
raise HTTPException(status_code=404, detail="Token not found")
conn.execute("UPDATE tokens SET revoked = 1 WHERE token = ?", (token,))
log_admin_action(conn, action="revoke_token", token=token, old_value="active", new_value="revoked")
conn.commit()
return AdminRevokeTokenResponse(ok=True, token=token, message="Token revoked")
finally:
conn.close()
@router.post("/set_status", response_model=AdminSetStatusResponse)
def admin_set_status(req: AdminSetStatusRequest):
_require_admin_key(req.admin_key)
email = req.email.strip().lower()
status = req.status.strip().lower()
if not email or "@" not in email:
raise HTTPException(status_code=400, detail="Invalid email")
if status not in ("active", "suspended", "cancelled"):
raise HTTPException(status_code=400, detail="Invalid status (use active|suspended|cancelled)")
conn = get_db()
try:
row = conn.execute("SELECT id, status FROM users WHERE email = ?", (email,)).fetchone()
if not row:
raise HTTPException(status_code=404, detail="User not found")
old_status = str(row["status"]) if row["status"] else ""
conn.execute("UPDATE users SET status = ? WHERE email = ?", (status, email))
log_admin_action(conn, action="set_status", email=email, old_value=old_status, new_value=status)
conn.commit()
return AdminSetStatusResponse(ok=True, email=email, status=status, message="Status updated")
finally:
conn.close()
@router.post("/audit/list", response_model=AdminAuditListResponse)
def admin_audit_list(req: AdminAuditListRequest):
_require_admin_key(req.admin_key)
limit = int(req.limit)
conn = get_db()
try:
rows = conn.execute(
"""
SELECT id, action, email, token, old_value, new_value, created_at
FROM admin_audit
ORDER BY id DESC
LIMIT ?
""",
(limit,),
).fetchall()
items = [
AdminAuditItem(
id=int(r["id"]),
action=str(r["action"]),
email=r["email"],
token=r["token"],
old_value=r["old_value"],
new_value=r["new_value"],
created_at=str(r["created_at"]),
)
for r in rows
]
return AdminAuditListResponse(ok=True, items=items)
finally:
conn.close()
return router