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}