Files
stealergram/web/routes/users.py
anti 4c104cddd2 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
2026-04-02 11:41:46 -03:00

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"}