Add web frontend with JWT auth, RBAC, SSE dashboard, and config editor
- 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
This commit is contained in:
82
web/routes/users.py
Normal file
82
web/routes/users.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
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"}
|
||||
Reference in New Issue
Block a user