| 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"]) |
|
|
|
|
| |
|
|
| def require_admin(user=Depends(require_auth)): |
| if user.get("role") != "admin": |
| raise HTTPException(status_code=403, detail="Admin access required") |
| return user |
|
|
|
|
| |
|
|
| @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)] |
|
|
| |
| 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() |
|
|
| |
| 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, |
| }, |
| } |
|
|
|
|
| |
|
|
| @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 |
|
|
|
|
| |
|
|
| @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") |
| |
| 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"} |
|
|
|
|
| |
|
|
| @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)} |
|
|
|
|
| |
|
|
| @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 = [ |
| 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), |
| } |
|
|
|
|
| |
|
|
| @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} |
|
|
|
|
| |
|
|
| @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} |
|
|