Skill: Route Analysis
Use this skill to analyse, audit, or modify HTTP and WebSocket routes in VoxBento. All routes live in
portal/routers/.
Route Categories
| Prefix | Type | Auth |
|---|---|---|
/ |
Public pages | None or cookie-optional |
/interpreter/* |
Booth pages | session_token or user_token cookie |
/listener/* |
Listener pages | session_token or user_token cookie |
/join/* |
Invite redemption | None (token in path) |
/register, /login, /logout, /account |
User auth | None / user_token |
/api/* |
REST API | Optional Bearer JWT or ?token= |
/admin/* |
Admin panel | admin_token or user_token with is_admin |
/ws/booth/* |
WebSocket coordination | Cookies + optional ?token= |
/ws/captions/* |
Caption WebSocket | None |
/static/* |
Static assets | None |
/healthz |
Health check | None |
Auth Patterns
User page routes
payload = get_booth_session(request) # checks session_token then user_token
if payload is None:
return safe_redirect(url=f'/login?next={path}', ...)
granted_role = await resolve_booth_role(payload, booth_id)
Admin routes
@app.get('/admin/...', dependencies=[Depends(require_admin)])
require_admin checks user_token (is_admin=True or event_admin membership) then falls back to admin_token.
API routes (optional auth)
_require_access(credentials, token) # passes if booth_access_token is unset
WebSocket
- Cookies read at connect time:
session_tokenthenuser_token. - Role resolved via
resolve_booth_role(payload, booth_id). - Token scope validated:
session_token.event_slug + language_codemust match the booth. - Connection rejected with code 4003 on scope mismatch; 4001 on invalid token.
Role Resolution (portal/auth.py)
resolve_booth_role(payload, booth_id) returns the most privileged applicable role:
super_admin— ifis_admin=Truein user token.event_admin— if user hasEventMembership.role == 'event_admin'for the booth's event.- Role from
BoothMembershipfor the specific booth. - Role from
EventMembershipfor the booth's event. - Role embedded in participant
session_token— but only ifevent_slug + language_codematch. - Returns
Noneif no applicable role found → 403 on page routes.
Redirect Safety
All redirects MUST use safe_redirect(url, status_code):
def safe_redirect(url: str, status_code: int) -> RedirectResponse:
url = url.replace('', '').strip()
parsed = urlparse(url)
if url and not parsed.netloc and not parsed.scheme and url.startswith('/'):
return RedirectResponse(url=url, status_code=status_code)
return RedirectResponse(url='/', status_code=status_code) # fallback
Never call RedirectResponse(url=user_input) directly.
Adding a New Route
- Add handler to
portal/routers/. - Use correct auth pattern (see above).
- Use
safe_redirectfor all redirects. - Return
templates.TemplateResponse(request, 'template.html', context)for HTML. - Raise
HTTPExceptionfor errors — never return raw error strings. - Add test to
tests/test_fastapi_app.py.
Critical Endpoints Reference
| Endpoint | Key behavior |
|---|---|
GET /join/{token} |
Validates + redeems invite token; sets session_token cookie; redirects to booth or listener |
GET /interpreter/{event_slug}/{language_code} |
Resolves Jitsi URL from DB, relay WHEP, creates MediaMTX path; passes granted_role to template |
GET /api/events/{slug}/booths/{lang}/whip-url |
Validates event ownership + active-interpreter status before returning WHIP URL |
WS /ws/booth/{booth_id} |
Full WebSocket lifecycle; role never from client; scope validated; disconnect cleans up participant |
WS /ws/captions/{booth_id} |
Open subscription; receives caption + booth:state; used by listener page |
POST /admin/.../.../transcription-settings |
Updates DB config; if booth live, stops and restarts transcription worker |
Common Route Bugs to Check
- Missing
safe_redirect→ open redirect risk. - Missing auth dependency on admin route → unauthenticated access.
data['role']used directly from WS message → role injection vulnerability.tokenpath param passed directly to DB query without validation → injection risk (mitigated byredeem_invite_tokenvalidation).next_url/nextquery param used in redirect withoutsafe_redirect→ open redirect.