- FastAPI + htmx + Jinja2 web frontend, started with --web flag - JWT HS256 auth (WEB_SECRET_KEY) with httpOnly cookies; access (15 min) + refresh (7 day) tokens; refresh rotation + JTI revocation in data/web.db - RBAC: superadmin > admin > reader enforced per route - Live SSE dashboard fed by tui/events broadcast queue - Config editor: keyword groups and channel list saved to data/runtime_config.json and hot-reloaded in-process (scorer.reload_from_config, signal_channel_changed) - config.py migrated to load groups/channels from runtime_config.json; falls back to hardcoded defaults when file absent - tui/events.py: subscribe/unsubscribe broadcast, set_bot_context/signal_channel_changed - utils/scorer.py: import config as _config (fixes local binding); reload_from_config() - utils/database.py: count_by_severity, recent_for_domains, count_by_severity_for_domains - 53 new tests (events bus, JWT lifecycle, web DB CRUD, RBAC enforcement, config round-trip); total 141 passing
83 lines
2.5 KiB
Python
83 lines
2.5 KiB
Python
"""
|
|
web/routes/users.py — User CRUD (superadmin only).
|
|
|
|
GET /users → list all users
|
|
POST /users → create a new user
|
|
PATCH /users/{id} → update role / password / active flag
|
|
DELETE /users/{id} → deactivate (cannot delete self or other superadmins)
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
from fastapi.responses import RedirectResponse
|
|
|
|
from web import db
|
|
from web.dependencies import require_role
|
|
from web.models import CreateUserRequest, UpdateUserRequest
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _templates(request: Request):
|
|
return request.app.state.templates
|
|
|
|
|
|
@router.get("/users")
|
|
async def list_users(request: Request, user=Depends(require_role("superadmin"))):
|
|
users = db.list_users()
|
|
return _templates(request).TemplateResponse(
|
|
request, "users.html",
|
|
{"user": dict(user), "users": [dict(u) for u in users]},
|
|
)
|
|
|
|
|
|
@router.post("/users")
|
|
async def create_user(
|
|
payload: CreateUserRequest,
|
|
_user=Depends(require_role("superadmin")),
|
|
):
|
|
try:
|
|
user_id = db.create_user(payload.username, payload.password, payload.role)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
|
|
return {"id": user_id}
|
|
|
|
|
|
@router.patch("/users/{user_id}")
|
|
async def update_user(
|
|
user_id: str,
|
|
payload: UpdateUserRequest,
|
|
acting_user=Depends(require_role("superadmin")),
|
|
):
|
|
target = db.get_user_by_id(user_id)
|
|
if target is None:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
# Cannot demote another superadmin via role patch
|
|
if target["role"] == "superadmin" and payload.role and payload.role != "superadmin":
|
|
raise HTTPException(status_code=403, detail="Cannot demote another superadmin")
|
|
|
|
updates: dict = {}
|
|
if payload.password is not None:
|
|
updates["password"] = payload.password
|
|
if payload.role is not None:
|
|
updates["role"] = payload.role
|
|
if payload.is_active is not None:
|
|
updates["is_active"] = 1 if payload.is_active else 0
|
|
|
|
db.update_user(user_id, **updates)
|
|
return {"status": "ok"}
|
|
|
|
|
|
@router.delete("/users/{user_id}")
|
|
async def deactivate_user(
|
|
user_id: str,
|
|
acting_user=Depends(require_role("superadmin")),
|
|
):
|
|
if user_id == acting_user["id"]:
|
|
raise HTTPException(status_code=403, detail="Cannot deactivate yourself")
|
|
target = db.get_user_by_id(user_id)
|
|
if target is None:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
db.deactivate_user(user_id)
|
|
return {"status": "ok"}
|