Bound credentials, brake password changes, cap bug reports, page the user list, structure every error (B-94, SEC-54, SEC-55, API-09, API-15, API-16, API-19, API-21, API-28, API-33, REG-12, AUD-42)

- login and password-change fields: at most 256 characters
- wrong current password: per-user brake (5 free, 2s doubling to 300s), 429 even for the
  right password while locked; password_changed audited in the same transaction
- POST /bug-reports: 10 per IP per hour, consumed only by a stored report
- GET /admin/users: limit/after_id paging, the panel walks the pages
- 401/403/404/405/422/503 all answer {code, message, params}; the 422 omits the input
- static files revalidated on every use (Cache-Control: no-cache)
- error.* keys for the new codes in all seven languages

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-18 00:59:25 +02:00
1 parent c1b192f6d1
commit 24e5fc3d8b
23 files changed
+374 -111

No files matched your search

+12 -21
View File
@@ -117,7 +117,6 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
modo: torna alla schermata di login.
*Codice:* `app/auth/dependencies.py:get_current_user` ·
*Test:* `tests/unit/test_users.py`, `tests/unit/test_bug_reports.py`
*Stato:* da implementare (B-94), per l'header assente · *Oggi:* l'assenza
dell'header è intercettata da FastAPI prima del codice applicativo e
risponde con il corpo non strutturato `{"detail": "Not authenticated"}`.
@@ -137,7 +136,6 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
(vedi [sicurezza.md](sicurezza.md)).
*Codice:* `app/api/routes/admin.py:require_admin` ·
*Test:* `tests/unit/test_admin.py`
*Stato:* da implementare (B-94) · *Oggi:* `403` con il corpo non
strutturato `{"detail": "invalid admin token"}`.
- **[API-09]** Intestazioni di cache:
@@ -156,7 +154,6 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
`app/main.py:user_guide`, `app/main.py:report_bug_page`,
`app/api/routes/rounds.py:round_stream`, `app/api/routes/qr.py:address_qr`,
`app/main.py` (`app.mount`)
*Stato:* da implementare (B-94), per i file statici · *Oggi:* i file
serviti dal mount non hanno `Cache-Control`, solo `ETag`/`Last-Modified`,
e il browser può riusarli senza rivalidarli.
@@ -169,8 +166,8 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
*Test:* `tests/unit/test_docs_disabled.py`
- **[API-11]** Dove un controllo dipende dall'indirizzo IP del chiamante (limiti
di login e registrazione, tetto per IP del flusso SSE e, da implementare con
B-94, quota per IP di `POST /bug-reports`), l'IP è l'**ultimo**
di login e registrazione, tetto per IP del flusso SSE e quota per IP di
`POST /bug-reports`), l'IP è l'**ultimo**
elemento di `X-Forwarded-For` (quello scritto dal proxy fidato), o in sua
assenza l'indirizzo della connessione, o `"unknown"`. Il motivo e il ruolo di
Caddy sono in [sicurezza.md](sicurezza.md).
@@ -238,11 +235,10 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
con `type: "json_invalid"`). Il valore ricevuto (`input`) **non** è
ripetuto nella risposta, così una password troppo lunga non torna indietro
in chiaro.
*Codice:* `app/main.py` (gestore di `RequestValidationError`, da scrivere),
*Codice:* `app/main.py` (gestore di `RequestValidationError`),
`app/static/app.js:apiErrorMessage` ·
*Test:* `tests/unit/test_users.py`, `tests/unit/test_admin.py`,
`tests/unit/test_bug_reports.py`
*Stato:* da implementare (B-94) · *Oggi:* `422` nel formato **nativo** di
FastAPI: `detail` è una **lista** di oggetti `{type, loc, msg, input, ctx?}`,
senza `code`; il frontend riconosce la lista e mostra il messaggio generico
tradotto `error.invalid_request`.
@@ -269,7 +265,6 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
`app/api/routes/admin.py:update_bug_report_status`,
`app/api/routes/rounds.py:round_stream`, `app/main.py` ·
*Test:* `tests/unit/test_admin.py`, `tests/unit/test_bug_reports.py`
*Stato:* da implementare (B-94) · *Oggi:* tutte le risposte della tabella
hanno `detail` come stringa inglese (colonna "Oggi"), e il frontend la
mostra così com'è.
@@ -334,7 +329,6 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
massimo 256 caratteri (oltre: `422 invalid_request`, prima di qualunque
hash Argon2 e senza consumare i contatori di fallimento). Lo username è
confrontato senza distinzione tra maiuscole e minuscole.
*Stato:* da implementare (B-94) · *Oggi:* nessun vincolo di lunghezza.
- **Risposta `200`:** `{"access_token": <JWT>, "address": <indirizzo>}`.
- **Errori:** `422`; `429 rate_limited` (`params.retry_after_seconds`,
arrotondato per eccesso al secondo); `401 invalid_credentials` —
@@ -399,7 +393,6 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
*Codice:* `app/api/routes/users.py:change_password`,
`app/auth/rate_limit.py:RateLimiter` ·
*Test:* `tests/unit/test_users.py`
*Stato:* da implementare (B-94) · *Oggi:* nessuna lunghezza massima, nessun
limite di frequenza (la password attuale si può provare senza limiti) e
nessuna voce di audit.
@@ -609,7 +602,7 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
`app/rounds/events.py:RoundEventBroadcaster.subscribe`,
`app/rounds/events.py:RoundEventBroadcaster.publish` ·
*Test:* `tests/unit/test_round_events.py`
*Stato:* da implementare (B-94, B-93) · *Oggi:* il `503` ha il corpo non
*Stato:* da implementare (B-93), per la chiusura dei flussi all'arresto
strutturato `{"detail": "too many concurrent update streams"}`, e un flusso
aperto non viene chiuso all'arresto: uvicorn lo attende finché Docker non
termina il processo.
@@ -720,7 +713,6 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
`app/api/routes/bug_reports.py:BugReportCreate`,
`app/auth/rate_limit.py:RollingQuota` ·
*Test:* `tests/unit/test_bug_reports.py`
*Stato:* da implementare (B-94) · *Oggi:* nessun limite di frequenza: un
anonimo può inserire segnalazioni senza limite.
#### GET /bug-reports/mine
@@ -841,7 +833,6 @@ dei parametri operativi in
"balance_sats", "created_at"}`; `balance_sats` è il saldo confermato.
*Codice:* `app/api/routes/admin.py:list_users` ·
*Test:* `tests/unit/test_admin.py`
*Stato:* da implementare (B-94) · *Oggi:* restituisce **tutti** gli utenti
in una sola risposta, senza paginazione né limite.
#### GET /admin/users/{user_id}/privkey
@@ -1029,7 +1020,7 @@ dei parametri operativi in
|---|---|---|---|---|---|
| `session_expired` | 401 | `app/auth/dependencies.py:get_current_user` | — | token illeggibile, scaduto, utente inesistente o token superato da un cambio/reset password | sì |
| `invalid_credentials` | 401 | `app/auth/routes.py:login` | — | username inesistente o password sbagliata (risposta identica) | sì |
| `rate_limited` | 429 | `app/auth/routes.py:register`, `app/auth/routes.py:login`; anche `app/api/routes/users.py:change_password` e `app/api/routes/bug_reports.py` *da implementare (B-94)* | `retry_after_seconds` | quota di registrazione, blocco per tentativi falliti, o quota di segnalazioni | sì |
| `rate_limited` | 429 | `app/auth/routes.py:register`, `app/auth/routes.py:login`; anche `app/api/routes/users.py:change_password` e `app/api/routes/bug_reports.py` | `retry_after_seconds` | quota di registrazione, blocco per tentativi falliti, o quota di segnalazioni | sì |
| `username_taken` | 409 | `app/auth/routes.py:register` | — | nome già usato (senza distinzione di maiuscole) | sì |
| `derivation_index_conflict` | 409 | `app/auth/routes.py:register` | — | nessun indice di derivazione libero dopo 5 tentativi | sì |
| `current_password_incorrect` | 401 | `app/api/routes/users.py:change_password` | — | password attuale errata | sì |
@@ -1053,14 +1044,14 @@ dei parametri operativi in
| `withdrawal_to_platform_address` | 400 | `app/withdrawals/service.py:request_withdrawal` | — | destinazione uguale a un indirizzo di montepremi, di riserva o delle commissioni | sì |
| `service_busy` | 503 | `app/bets/service.py:place_bet`, `app/withdrawals/service.py:request_withdrawal`, `app/auth/routes.py:register` | — | il database non ha concesso il lock di scrittura entro il timeout; nulla è stato trasmesso, si può riprovare | sì |
| `registration_unavailable` | 503 | `app/auth/routes.py:register` | — | registrazione sospesa finché il controllo degli indici di derivazione dopo un ripristino non è concluso o risolto ([registrazione-e-depositi.md](registrazione-e-depositi.md), B-90) | sì |
| `not_authenticated` | 401 | dipendenza Bearer di FastAPI | — | header `Authorization` assente — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
| `invalid_admin_token` | 403 | `app/api/routes/admin.py:require_admin` | — | token admin assente o errato — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
| `user_not_found` | 404 | `app/api/routes/admin.py` | — | utente inesistente — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
| `bug_report_not_found` | 404 | `app/api/routes/admin.py` | — | segnalazione inesistente — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
| `not_authenticated` | 401 | dipendenza Bearer di FastAPI | — | header `Authorization` assente | sì |
| `invalid_admin_token` | 403 | `app/api/routes/admin.py:require_admin` | — | token admin assente o errato | sì |
| `user_not_found` | 404 | `app/api/routes/admin.py` | — | utente inesistente | sì |
| `bug_report_not_found` | 404 | `app/api/routes/admin.py` | — | segnalazione inesistente | sì |
| `round_not_found` | 404 | `app/api/routes/rounds.py` (`GET /rounds/{id}/proof`) | — | round inesistente | sì |
| `not_found` | 404 | gestore delle eccezioni HTTP in `app/main.py` | — | percorso inesistente — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
| `method_not_allowed` | 405 | gestore delle eccezioni HTTP in `app/main.py` | — | metodo non ammesso — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
| `too_many_streams` | 503 | `app/api/routes/rounds.py` (SSE) | — | tetto globale degli stream raggiunto — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
| `not_found` | 404 | gestore delle eccezioni HTTP in `app/main.py` | — | percorso inesistente | sì |
| `method_not_allowed` | 405 | gestore delle eccezioni HTTP in `app/main.py` | — | metodo non ammesso | sì |
| `too_many_streams` | 503 | `app/api/routes/rounds.py` (SSE) | — | tetto globale degli stream raggiunto | sì |
Due codici di `app/wallet/psbt_builder.py` — `winner_share_below_network_fee`
e `commission_below_dust_limit` — riguardano solo la costruzione del