nikeshn commited on
Commit
fdc2f20
Β·
verified Β·
1 Parent(s): 9043adc

Update src/api/admin.py

Browse files
Files changed (1) hide show
  1. src/api/admin.py +94 -23
src/api/admin.py CHANGED
@@ -1,50 +1,121 @@
1
- from fastapi import APIRouter, HTTPException, Request, Response
2
- from pydantic import BaseModel
3
  import secrets
4
  import time
 
 
 
 
 
 
5
 
 
6
  router = APIRouter()
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  class LoginRequest(BaseModel):
9
  password: str
10
 
 
11
  class ConfigUpdate(BaseModel):
12
  key: str
13
  value: str
14
 
15
- # Simple session storage (in-memory for HF single instance)
16
- admin_sessions = {}
17
 
18
  @router.post("/auth")
19
  async def admin_auth(req: LoginRequest):
20
- from src.config import get_settings
21
  settings = get_settings()
22
-
 
 
 
23
  if req.password != settings.admin_password:
24
  raise HTTPException(status_code=401, detail="Invalid password")
25
-
26
  token = secrets.token_hex(32)
27
- admin_sessions[token] = time.time() + 86400 # 24 hours
28
-
29
- resp = Response(content='{"status": "ok"}')
30
- resp.set_cookie(key="admin_session", value=token, httponly=True, max_age=86400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  return resp
32
 
33
- @router.get("/config")
 
34
  async def get_config():
35
- from src.config import get_settings
36
  settings = get_settings()
37
  return {
38
  "max_results": settings.max_results,
39
- "maintenance_mode": settings.maintenance_mode
 
40
  }
41
 
42
- @router.post("/config")
43
- async def update_config(req: ConfigUpdate, request: Request):
44
- # Verify session
45
- token = request.cookies.get("admin_session")
46
- if not token or token not in admin_sessions:
47
- raise HTTPException(status_code=401, detail="Not authenticated")
48
-
49
- # In real implementation, update DB or env
50
- return {"status": "ok", "updated": req.key}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
 
2
  import secrets
3
  import time
4
+ from typing import Annotated
5
+
6
+ from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response
7
+ from pydantic import BaseModel
8
+
9
+ from src.config import get_settings
10
 
11
+ logger = logging.getLogger(__name__)
12
  router = APIRouter()
13
 
14
+ # In-memory session store β€” single instance on HF Spaces free tier
15
+ # Resets on every container restart (expected behaviour)
16
+ _admin_sessions: dict[str, float] = {} # token β†’ expiry timestamp
17
+
18
+
19
+ # ── Auth Dependency ────────────────────────────────────────────────────────────
20
+
21
+ def _cleanup_sessions():
22
+ """Evict expired tokens to prevent unbounded growth."""
23
+ now = time.time()
24
+ expired = [t for t, exp in _admin_sessions.items() if exp < now]
25
+ for t in expired:
26
+ del _admin_sessions[t]
27
+
28
+
29
+ def require_admin(admin_session: Annotated[str | None, Cookie()] = None):
30
+ """FastAPI dependency β€” raises 401 if session is missing or expired."""
31
+ _cleanup_sessions()
32
+ if not admin_session or admin_session not in _admin_sessions:
33
+ raise HTTPException(status_code=401, detail="Not authenticated")
34
+ if _admin_sessions[admin_session] < time.time():
35
+ del _admin_sessions[admin_session]
36
+ raise HTTPException(status_code=401, detail="Session expired")
37
+
38
+
39
+ # ── Request Models ─────────────────────────────────────────────────────────────
40
+
41
  class LoginRequest(BaseModel):
42
  password: str
43
 
44
+
45
  class ConfigUpdate(BaseModel):
46
  key: str
47
  value: str
48
 
49
+
50
+ # ── Endpoints ──────────────────────────────────────────────────────────────────
51
 
52
  @router.post("/auth")
53
  async def admin_auth(req: LoginRequest):
 
54
  settings = get_settings()
55
+
56
+ if not settings.admin_password:
57
+ raise HTTPException(status_code=503, detail="Admin password not configured")
58
+
59
  if req.password != settings.admin_password:
60
  raise HTTPException(status_code=401, detail="Invalid password")
61
+
62
  token = secrets.token_hex(32)
63
+ _admin_sessions[token] = time.time() + 86400 # 24 hours
64
+
65
+ resp = Response(content='{"status": "ok"}', media_type="application/json")
66
+ resp.set_cookie(
67
+ key="admin_session",
68
+ value=token,
69
+ httponly=True,
70
+ secure=True, # HTTPS only β€” HF Spaces always serves HTTPS
71
+ samesite="strict",
72
+ max_age=86400,
73
+ )
74
+ return resp
75
+
76
+
77
+ @router.post("/logout")
78
+ async def admin_logout(admin_session: Annotated[str | None, Cookie()] = None):
79
+ if admin_session and admin_session in _admin_sessions:
80
+ del _admin_sessions[admin_session]
81
+ resp = Response(content='{"status": "ok"}', media_type="application/json")
82
+ resp.delete_cookie("admin_session")
83
  return resp
84
 
85
+
86
+ @router.get("/config", dependencies=[Depends(require_admin)])
87
  async def get_config():
 
88
  settings = get_settings()
89
  return {
90
  "max_results": settings.max_results,
91
+ "maintenance_mode": settings.maintenance_mode,
92
+ "environment": settings.environment,
93
  }
94
 
95
+
96
+ @router.post("/config", dependencies=[Depends(require_admin)])
97
+ async def update_config(req: ConfigUpdate):
98
+ # Runtime-only config update β€” does not persist across restarts.
99
+ # To persist, write to a file under /data (paid HF tier) or use an external store.
100
+ allowed_keys = {"maintenance_mode", "max_results"}
101
+ if req.key not in allowed_keys:
102
+ raise HTTPException(status_code=400, detail=f"Key '{req.key}' is not configurable at runtime")
103
+ # TODO: update a shared runtime config object here
104
+ logger.info(f"Admin config update: {req.key} = {req.value}")
105
+ return {"status": "ok", "updated": req.key, "note": "runtime only β€” resets on restart"}
106
+
107
+
108
+ @router.get("/rag-status", dependencies=[Depends(require_admin)])
109
+ async def rag_status():
110
+ """Admin view of RAG initialization state."""
111
+ try:
112
+ from app import rag_service
113
+ return rag_service.get_stats()
114
+ except Exception as e:
115
+ return {"error": str(e), "ready": False}
116
+
117
+
118
+ @router.get("/sessions", dependencies=[Depends(require_admin)])
119
+ async def session_info():
120
+ _cleanup_sessions()
121
+ return {"active_sessions": len(_admin_sessions)}