corpusdb / app /admin.py
mrsavage1's picture
Upload 61 files
e68ff31 verified
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse
from app.auth import require_auth, get_user_store
from app.user_manager import user_manager
from app.audit_manager import audit
from app.security_logger import security_logger
router = APIRouter(prefix="/admin", tags=["admin"])
# ── Guard ─────────────────────────────────────────────────────────
def require_admin(user=Depends(require_auth)):
if user.get("role") != "admin":
raise HTTPException(status_code=403, detail="Admin access required")
return user
# ── Dashboard Stats ───────────────────────────────────────────────
@router.get("/stats")
def admin_stats(user=Depends(require_admin)):
"""System-wide stats: users, sessions, databases, tables."""
from app.table_manager import table_manager
from app.metadata import metadata
users = user_manager.list_users()
active_users = [u for u in users if u.get("is_active", True)]
# Count active sessions across all users
user_manager._init()
conn = user_manager._get_conn()
from datetime import datetime
now = datetime.utcnow().isoformat()
session_count = conn.execute(
"SELECT COUNT(*) FROM sessions WHERE expires_at > ?", [now]
).fetchone()[0]
conn.close()
# Count all databases and tables across all workspaces
total_dbs = 0
total_tables = 0
for u in users:
try:
store = get_user_store(u)
dbs = metadata.list_databases(store)
db_list = dbs.get("databases", []) if isinstance(dbs, dict) else []
total_dbs += len(db_list)
tables = table_manager.list(store)
total_tables += len(tables) if isinstance(tables, list) else 0
except Exception:
pass
return {
"ok": True,
"stats": {
"total_users": len(users),
"active_users": len(active_users),
"active_sessions": session_count,
"total_databases": total_dbs,
"total_tables": total_tables,
},
}
# ── User Management ───────────────────────────────────────────────
@router.get("/users")
def list_users(user=Depends(require_admin)):
"""List all users with safe fields."""
users = user_manager.list_users()
return {
"ok": True,
"users": [
{
"username": u.get("username"),
"email": u.get("email"),
"full_name": u.get("full_name") or "",
"role": u.get("role", "viewer"),
"is_active": bool(u.get("is_active", True)),
"created_at": str(u.get("created_at") or ""),
"last_login": str(u.get("last_login") or ""),
"workspace_id": u.get("workspace_id"),
}
for u in users
],
}
@router.get("/users/{username}")
def get_user(username: str, user=Depends(require_admin)):
u = user_manager.get_user(username)
if not u:
raise HTTPException(status_code=404, detail="User not found")
u.pop("password", None)
return {"ok": True, "user": u}
@router.put("/users/{username}")
def update_user(username: str, payload: dict, user=Depends(require_admin)):
"""Update role, active status, full_name, email, department, phone."""
if username == user.get("username") and payload.get("role") and payload["role"] != "admin":
return {"ok": False, "error": "Cannot demote yourself"}
allowed = {"email", "full_name", "role", "is_active", "department", "phone", "password"}
updates = {k: v for k, v in payload.items() if k in allowed}
if not updates:
return {"ok": False, "error": "No valid fields to update"}
return user_manager.update_user(username, updates)
@router.delete("/users/{username}")
def delete_user(username: str, user=Depends(require_admin)):
if username == user.get("username"):
return {"ok": False, "error": "Cannot delete yourself"}
result = user_manager.delete_user(username)
if result.get("ok"):
audit.log("admin_delete_user", actor=user["username"], target=username)
return result
@router.post("/users/{username}/toggle-active")
def toggle_user_active(username: str, user=Depends(require_admin)):
if username == user.get("username"):
return {"ok": False, "error": "Cannot deactivate yourself"}
u = user_manager.get_user(username)
if not u:
raise HTTPException(status_code=404, detail="User not found")
new_state = not bool(u.get("is_active", True))
result = user_manager.update_user(username, {"is_active": new_state})
audit.log(
"admin_toggle_user",
actor=user["username"],
target=username,
after={"is_active": new_state},
)
return result
@router.post("/users/{username}/reset-password")
def reset_password(username: str, payload: dict, user=Depends(require_admin)):
new_password = payload.get("password", "").strip()
if len(new_password) < 8:
return {"ok": False, "error": "Password must be at least 8 characters"}
result = user_manager.update_user(username, {"password": new_password})
if result.get("ok"):
audit.log("admin_reset_password", actor=user["username"], target=username)
return result
@router.post("/users/{username}/promote")
def promote_to_admin(username: str, user=Depends(require_admin)):
result = user_manager.update_user(username, {"role": "admin"})
if result.get("ok"):
audit.log("admin_promote_user", actor=user["username"], target=username)
return result
@router.post("/users/{username}/demote")
def demote_from_admin(username: str, user=Depends(require_admin)):
if username == user.get("username"):
return {"ok": False, "error": "Cannot demote yourself"}
result = user_manager.update_user(username, {"role": "viewer"})
if result.get("ok"):
audit.log("admin_demote_user", actor=user["username"], target=username)
return result
# ── Session Management ────────────────────────────────────────────
@router.get("/sessions")
def list_sessions(user=Depends(require_admin)):
"""List all active sessions."""
user_manager._init()
conn = user_manager._get_conn()
from datetime import datetime
now = datetime.utcnow().isoformat()
rows = conn.execute(
"SELECT token, username, workspace_id, created_at, expires_at FROM sessions WHERE expires_at > ? ORDER BY created_at DESC",
[now],
).fetchdf()
conn.close()
sessions = rows.to_dict("records")
# Mask token
for s in sessions:
s["token"] = s["token"][:8] + "..." if s.get("token") else ""
s["created_at"] = str(s.get("created_at") or "")
s["expires_at"] = str(s.get("expires_at") or "")
return {"ok": True, "sessions": sessions, "total": len(sessions)}
@router.delete("/sessions/{username}")
def revoke_user_sessions(username: str, user=Depends(require_admin)):
"""Revoke all sessions for a user (force logout)."""
user_manager._init()
conn = user_manager._get_conn()
conn.execute("DELETE FROM sessions WHERE username = ?", [username])
conn.close()
audit.log("admin_revoke_sessions", actor=user["username"], target=username)
return {"ok": True, "message": f"All sessions for '{username}' revoked"}
@router.delete("/sessions")
def revoke_all_sessions(user=Depends(require_admin)):
"""Revoke ALL sessions except the current admin's."""
user_manager._init()
conn = user_manager._get_conn()
conn.execute(
"DELETE FROM sessions WHERE username != ?", [user["username"]]
)
conn.close()
audit.log("admin_revoke_all_sessions", actor=user["username"])
return {"ok": True, "message": "All other sessions revoked"}
# ── Audit Logs ────────────────────────────────────────────────────
@router.get("/audit")
def get_audit_logs(limit: int = 200, user=Depends(require_admin)):
"""Get recent audit log entries."""
logs = audit.list()
return {"ok": True, "logs": logs[-limit:], "total": len(logs)}
@router.get("/audit/user/{username}")
def get_user_audit(username: str, user=Depends(require_admin)):
"""Get audit logs filtered by actor."""
logs = [e for e in audit.list() if e.get("actor") == username]
return {"ok": True, "logs": logs[-100:], "total": len(logs)}
# ── Security Events ───────────────────────────────────────────────
@router.get("/security/events")
def get_security_events(limit: int = 100, event_type: str = None, username: str = None, user=Depends(require_admin)):
"""Get security events with optional filters."""
events = security_logger.get_recent_events(limit=limit, event_type=event_type, username=username)
return {"ok": True, "events": events, "total": len(events)}
@router.get("/security/summary")
def security_summary(user=Depends(require_admin)):
"""Summary of security events by type."""
events = security_logger.get_recent_events(limit=10000)
summary = {}
for e in events:
t = e.get("event_type", "UNKNOWN")
summary[t] = summary.get(t, 0) + 1
# Recent failures
recent_failures = [
e for e in events[-500:]
if e.get("event_type") in ("AUTH_FAILURE", "AUTH_BLOCKED", "SQL_INJECTION_ATTEMPT", "PATH_TRAVERSAL_ATTEMPT")
]
return {
"ok": True,
"event_counts": summary,
"recent_threats": recent_failures[-20:],
"total_events": len(events),
}
# ── Workspace / Database Overview ─────────────────────────────────
@router.get("/workspaces")
def list_all_workspaces(user=Depends(require_admin)):
"""List all user workspaces with database/table counts."""
from app.table_manager import table_manager
from app.metadata import metadata
users = user_manager.list_users()
result = []
for u in users:
try:
store = get_user_store(u)
dbs = metadata.list_databases(store)
db_list = dbs.get("databases", []) if isinstance(dbs, dict) else []
tables = table_manager.list(store)
table_count = len(tables) if isinstance(tables, list) else 0
except Exception:
db_list = []
table_count = 0
result.append({
"username": u.get("username"),
"workspace_id": u.get("workspace_id"),
"role": u.get("role", "viewer"),
"is_active": bool(u.get("is_active", True)),
"databases": len(db_list),
"tables": table_count,
})
return {"ok": True, "workspaces": result}
# ── System Maintenance ────────────────────────────────────────────
@router.post("/maintenance/cleanup-sessions")
def cleanup_expired_sessions(user=Depends(require_admin)):
"""Remove all expired sessions from the database."""
user_manager.cleanup_expired_sessions()
audit.log("admin_cleanup_sessions", actor=user["username"])
return {"ok": True, "message": "Expired sessions cleaned up"}
@router.post("/maintenance/sync-hf")
def force_hf_sync(user=Depends(require_admin)):
"""Force sync system metadata to HuggingFace."""
try:
from app.system_persistence import system_persistence
system_persistence.force_sync()
audit.log("admin_force_sync", actor=user["username"])
return {"ok": True, "message": "HuggingFace sync triggered"}
except Exception as e:
return {"ok": False, "error": str(e)}
@router.get("/maintenance/system-info")
def system_info(user=Depends(require_admin)):
"""Basic system info: Python version, disk, memory."""
import sys
import os
import platform
info = {
"python_version": sys.version,
"platform": platform.system(),
"cwd": os.getcwd(),
}
try:
import psutil
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
info["memory"] = {
"total_mb": round(mem.total / 1024 / 1024),
"used_mb": round(mem.used / 1024 / 1024),
"percent": mem.percent,
}
info["disk"] = {
"total_gb": round(disk.total / 1024 / 1024 / 1024, 1),
"used_gb": round(disk.used / 1024 / 1024 / 1024, 1),
"percent": disk.percent,
}
except ImportError:
info["note"] = "Install psutil for memory/disk stats"
return {"ok": True, "system": info}