← Back to the README · All documentation · Italiano
Users and roles, sessions, protections and going online.
The whole API, except sign-in, requires an authenticated user. The dashboard's static files are public but contain no data.
Roles
| Role | Can do |
|---|---|
admin |
Everything: view the data, update news and markets, ask Jev for forecasts (paid) |
viewer |
View news, markets, forecasts and calibration |
User management (there is no public sign-up):
python -m backend.auth.cli create-user alice --role admin # asks for the password
python -m backend.auth.cli create-user bob # viewer
python -m backend.auth.cli list-users
python -m backend.auth.cli set-password alice # also closes all her sessions
python -m backend.auth.cli disable bob # disables and signs out
python -m backend.auth.cli enable bob
python -m backend.auth.cli revoke-sessions aliceThe command-line messages follow APP_LANGUAGE.
Alternatively, at the first start with no users an admin is created from ADMIN_USERNAME and
ADMIN_PASSWORD. After the first start remove ADMIN_PASSWORD from the .env file.
How it works
- Passwords: Argon2id hash, at least 12 characters, they cannot contain the username. Hashes with old parameters are updated at the next sign-in.
- Server-side sessions: the
nm_sessioncookie contains a random 256-bit token; the database only holds its SHA-256 hash. The cookie isHttpOnly,SameSite=LaxandSecure(except onlocalhostover HTTP). - Expiry: each session lasts at most
SESSION_TTL_HOURSand ends afterSESSION_IDLE_MINUTESof inactivity. Sign-out, password change and disabling the user revoke it right away. - CSRF: every request that changes data must send the session's token in the
X-CSRF-Tokenheader. The dashboard does it by itself. - Sign-in attempts: after
LOGIN_MAX_ATTEMPTSfailures per IP and username, orLOGIN_MAX_ATTEMPTS_PER_IPper IP, sign-in is blocked forLOGIN_WINDOW_MINUTES. The answer is the same whether the user exists or not, and takes the same time. - Security headers: Content-Security-Policy without inline scripts,
X-Frame-Options: DENY,nosniff, HSTS when the request arrives over HTTPS. - CORS: off. The dashboard is on the same origin; any external origins must be listed in
CORS_ORIGINS(the*wildcard is ignored).
Going online
- Put the app behind HTTPS: the simplest way is Cloudflare Tunnel, ready in the compose file
(see Deploy on a VPS, with
CLIENT_IP_HEADER=CF-Connecting-IP); alternatively a reverse proxy (Caddy, nginx, Traefik). - With a reverse proxy start uvicorn with
--proxy-headers(already in the Dockerfile), so the attempt limit sees the client's real IP. If the proxy does not run on the same machine, add--forwarded-allow-ipswith its address. - Set
API_DOCS_ENABLED=falseso as not to publish the API schema. - The attempt limit is in memory: with several workers each one has its own counters. With several workers add a limit on the proxy too.