Skip to content

Commit 82c679c

Browse files
authored
Add password-protected admin panel (#444)
* Add password-protected admin panel Signed-off-by: Trevor Grant <[email protected]> * config changes Signed-off-by: Trevor Grant <[email protected]> --------- Signed-off-by: Trevor Grant <[email protected]>
1 parent 69f5461 commit 82c679c

File tree

9 files changed

+365
-8
lines changed

9 files changed

+365
-8
lines changed

webapp/packages/api/user-service/config/__init__.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,21 @@
33

44
load_dotenv()
55

6+
7+
def _get_bool_env(var_name: str, default: bool = False) -> bool:
8+
value = os.getenv(var_name)
9+
if value is None:
10+
return default
11+
return value.lower() in ("1", "true", "yes", "on")
12+
13+
614
class Settings:
715
APP_ENV: str = os.getenv("APP_ENV", "local")
816
STORAGE_PROVIDER: str = os.getenv("STORAGE_PROVIDER", "local")
9-
17+
18+
ADMIN_PANEL_ENABLED: bool = _get_bool_env("ADMIN_PANEL_ENABLED", False)
19+
ADMIN_PANEL_PASSWORD: str = os.getenv("ADMIN_PANEL_PASSWORD", "password")
20+
1021
# S3/MinIO Settings
1122
S3_ENDPOINT_URL: str | None = os.getenv("S3_ENDPOINT_URL")
1223
AWS_ACCESS_KEY_ID: str | None = os.getenv("AWS_ACCESS_KEY_ID")
@@ -20,10 +31,9 @@ class Settings:
2031
COUCHDB_USER: str | None = os.getenv("COUCHDB_USER")
2132
COUCHDB_PASSWORD: str | None = os.getenv("COUCHDB_PASSWORD")
2233
# AWS CloudWatch Logging Settings
23-
CLOUDWATCH_LOG_GROUP_NAME: str | None = os.getenv("CLOUDWATCH_LOG_GROUP_NAME"
24-
)
34+
CLOUDWATCH_LOG_GROUP_NAME: str | None = os.getenv("CLOUDWATCH_LOG_GROUP_NAME")
2535
# Google Cloud Settings
2636
GCP_PROJECT_ID: str | None = os.getenv("GCP_PROJECT_ID")
2737

2838

29-
settings = Settings()
39+
settings = Settings()

webapp/packages/api/user-service/config/gemini/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"tool_config": {"codeExecution": {}}
3434
}
3535
]
36-
}
36+
},
3737
"gemini-2.5-pro": {
3838
"parameters": {
3939
"temperature": {

webapp/packages/api/user-service/config/openai/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"description": "Reasoning Effort: Effort level for reasoning during generation"
1515
},
1616
}
17-
}
17+
},
1818
"gpt-5": {
1919
"api_style": "responses",
2020
"returns_thoughts": True, # reasoning traces supported by GPT-5/o-series

webapp/packages/api/user-service/main.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# webapp/packages/api/user-service/main.py
2-
from fastapi import APIRouter, FastAPI, UploadFile, File, Depends, HTTPException, BackgroundTasks, Request
2+
from fastapi import APIRouter, FastAPI, UploadFile, File, Depends, HTTPException, BackgroundTasks, Request, Header
33
from fastapi.responses import JSONResponse
44
from fastapi.middleware.cors import CORSMiddleware
55
from typing import Annotated, Optional, Dict, Any, List
@@ -150,6 +150,14 @@ class AddUsageRequest(BaseModel):
150150

151151
model_config = ConfigDict(populate_by_name=True)
152152

153+
154+
class AdminUpdateUserRequest(BaseModel):
155+
monthly_allowance: Optional[float] = Field(default=None, alias="monthlyAllowance")
156+
allowance_reset_date: Optional[float] = Field(default=None, alias="allowanceResetDate")
157+
spend_remaining: Optional[float] = Field(default=None, alias="spendRemaining")
158+
159+
model_config = ConfigDict(populate_by_name=True)
160+
153161
# Import models after defining local ones to avoid circular dependencies
154162
from models.chat import ChatRequest, ChatMessage, ChatResponse, ProviderConfig, SessionData
155163

@@ -165,6 +173,14 @@ def get_logger() -> ObservabilityService:
165173
def get_user_service_dep(db: DatabaseService = Depends(get_db)) -> UserService:
166174
return get_user_service(db)
167175

176+
177+
def require_admin_access(admin_password: str | None = Header(default=None, alias="X-Admin-Password")):
178+
if not settings.ADMIN_PANEL_ENABLED:
179+
raise HTTPException(status_code=403, detail="Admin panel is disabled")
180+
181+
if admin_password != settings.ADMIN_PANEL_PASSWORD:
182+
raise HTTPException(status_code=401, detail="Invalid admin password")
183+
168184
# Background task for LLM processing
169185
async def process_chat(ticket_id: str, request: ChatRequest, user: dict, req: Request):
170186
# Background tasks don't have access to dependency injection, so we get service instances directly
@@ -394,6 +410,29 @@ def get_current_user_profile(user: dict = Depends(get_current_user), user_servic
394410
return user_service.get_user(user.get("uid", "anonymous"), user)
395411

396412

413+
@router.get("/admin/users", response_model=List[User])
414+
def list_all_users(
415+
user_service: UserService = Depends(get_user_service_dep),
416+
_: None = Depends(require_admin_access),
417+
):
418+
return user_service.list_users()
419+
420+
421+
@router.put("/admin/users/{user_id}", response_model=User)
422+
def update_user_allowances(
423+
user_id: str,
424+
request: AdminUpdateUserRequest,
425+
user_service: UserService = Depends(get_user_service_dep),
426+
_: None = Depends(require_admin_access),
427+
):
428+
return user_service.update_user_usage_info(
429+
user_id,
430+
monthly_allowance=request.monthly_allowance,
431+
allowance_reset_date=request.allowance_reset_date,
432+
spend_remaining=request.spend_remaining,
433+
)
434+
435+
397436
@router.put("/users/me/monthly-allowance", response_model=User)
398437
def set_monthly_allowance(
399438
request: UpdateMonthlyAllowanceRequest,

webapp/packages/api/user-service/services/user_service.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import datetime
2-
from typing import Optional, Any
2+
from typing import Optional, Any, List
33

44
from fastapi import HTTPException
55

@@ -34,6 +34,9 @@ def get_user(self, user_id: str, basic_info: Optional[dict] = None) -> User:
3434
user = self._create_default_user(user_id, basic_info)
3535
return self.save_user(user)
3636

37+
def list_users(self) -> List[User]:
38+
return [User(**user_doc) for user_doc in self.db.list_all("users")]
39+
3740
def save_user(self, user: User) -> User:
3841
user.updated_at = datetime.utcnow()
3942
saved = self.db.save("users", user.id, user.model_dump(by_alias=True, mode="json"))
@@ -69,6 +72,30 @@ def update_spend_remaining(self, user_id: str, spend_remaining: float, basic_inf
6972
user.usage_info.spend_remaining = spend_remaining
7073
return self.save_user(user)
7174

75+
def update_user_usage_info(
76+
self,
77+
user_id: str,
78+
*,
79+
monthly_allowance: Optional[float] = None,
80+
allowance_reset_date: Optional[float] = None,
81+
spend_remaining: Optional[float] = None,
82+
basic_info: Optional[dict] = None,
83+
) -> User:
84+
user = self.get_user(user_id, basic_info)
85+
86+
if monthly_allowance is not None:
87+
user.usage_info.monthly_allowance = monthly_allowance
88+
if user.usage_info.spend_remaining > monthly_allowance:
89+
user.usage_info.spend_remaining = monthly_allowance
90+
91+
if allowance_reset_date is not None:
92+
user.usage_info.allowance_reset_date = allowance_reset_date
93+
94+
if spend_remaining is not None:
95+
user.usage_info.spend_remaining = spend_remaining
96+
97+
return self.save_user(user)
98+
7299
def add_usage(self, user_id: str, response_cost: float, metadata: Optional[Any] = None, basic_info: Optional[dict] = None) -> User:
73100
user = self.get_user(user_id, basic_info)
74101
user.usage_info.usage.append(UsageEntry(responseCost=response_cost, metadata=metadata))

webapp/packages/webui/src/components/ProfileMenu.jsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import AccountCircle from '@mui/icons-material/AccountCircle';
55
import { useNavigate } from 'react-router-dom';
66
import { useAuth } from '../contexts/AuthContext';
77

8+
const isAdminPanelEnabled = () => {
9+
const envValue = import.meta.env.VITE_ADMIN_PANEL_ENABLED
10+
?? (typeof process !== 'undefined' ? process.env.ADMIN_PANEL_ENABLED : undefined)
11+
?? 'false';
12+
const raw = envValue.toString();
13+
return raw.toLowerCase() === 'true';
14+
};
15+
816
const ProfileMenu = () => {
917
const { logout } = useAuth();
1018
const [anchorEl, setAnchorEl] = useState(null);
@@ -58,6 +66,9 @@ const ProfileMenu = () => {
5866
<MenuItem onClick={() => handleNavigate('/profile/basic')}>Basic Info</MenuItem>
5967
<MenuItem onClick={() => handleNavigate('/profile/usage')}>Usage</MenuItem>
6068
<MenuItem onClick={() => handleNavigate('/profile/billing')}>Billing</MenuItem>
69+
{isAdminPanelEnabled() && (
70+
<MenuItem onClick={() => handleNavigate('/admin')}>Admin Panel</MenuItem>
71+
)}
6172
<Divider />
6273
<MenuItem onClick={handleLogout}>Logout</MenuItem>
6374
</Menu>

webapp/packages/webui/src/config/routesConfig.jsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,17 @@ import SaveDemoScreen from '../pages/DemoCreationFlow/SaveDemoScreen';
2121
import { AgentCreationFlowProvider } from '../pages/AgentCreationFlow/AgentCreationFlowContext';
2222
import { DemoCreationFlowProvider } from '../pages/DemoCreationFlow/DemoCreationFlowContext';
2323
import ProfilePage from '../pages/ProfilePage';
24+
import AdminPanel from '../pages/AdminPanel';
2425
// import extendRoutes from '../extensions/routes/routeExtensions';
2526

27+
const isAdminPanelEnabled = () => {
28+
const envValue = import.meta.env.VITE_ADMIN_PANEL_ENABLED
29+
?? (typeof process !== 'undefined' ? process.env.ADMIN_PANEL_ENABLED : undefined)
30+
?? 'false';
31+
const raw = envValue.toString();
32+
return raw.toLowerCase() === 'true';
33+
};
34+
2635
export const defaultRoutes = [
2736
{
2837
path: '/login',
@@ -110,6 +119,13 @@ export const defaultRoutes = [
110119
},
111120
];
112121

122+
if (isAdminPanelEnabled()) {
123+
defaultRoutes.push({
124+
path: '/admin',
125+
element: <AdminPanel />,
126+
});
127+
}
128+
113129
import { routes as extensionRoutes } from '../extensions';
114130

115131
export const loadRoutesConfig = () => {

0 commit comments

Comments
 (0)