update
This commit is contained in:
204
backup 24.2.26 - Kopie/admin_routes.py
Normal file
204
backup 24.2.26 - Kopie/admin_routes.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user