temp
This commit is contained in:
1 parent
61efbb0e37
commit
c5e7daa9c7
15 files changed
+4514
-1172
No files matched your search
+344
-106
@@ -34,6 +34,7 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
| `POST` | `/withdrawals` | Bearer | [API-24](#post-withdrawals) |
|
||||
| `GET` | `/rounds/current` | facoltativa | [API-25](#get-roundscurrent) |
|
||||
| `GET` | `/rounds/stream` | nessuna | [API-26](#get-roundsstream) |
|
||||
| `GET` | `/rounds/{round_id}/proof` | nessuna | [API-45](#get-roundsround_idproof) — *da implementare (B-73)* |
|
||||
| `GET` | `/qr/{address}` | nessuna | [API-27](#get-qraddress) |
|
||||
| `POST` | `/bug-reports` | facoltativa | [API-28](#post-bug-reports) |
|
||||
| `GET` | `/bug-reports/mine` | Bearer | [API-29](#get-bug-reportsmine) |
|
||||
@@ -49,7 +50,9 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
| `GET` | `/admin/pending-transactions` | admin | [API-38](#get-adminpending-transactions) |
|
||||
| `GET` | `/admin/bug-reports` | admin | [API-39](#get-adminbug-reports) |
|
||||
| `POST` | `/admin/bug-reports/{report_id}/status` | admin | [API-40](#post-adminbug-reportsreport_idstatus) |
|
||||
| `GET` | `/admin/electrum` | admin | [API-47](#get-adminelectrum) — *da implementare (B-86, B-92)* |
|
||||
| `GET` | `/health` | nessuna | [API-41](#get-health) |
|
||||
| `GET` | `/health/ready` | nessuna | [API-46](#get-healthready) — *da implementare (B-92)* |
|
||||
| `GET` | `/`, `/admin`, `/guida`, `/report-bug` | nessuna | [API-42](#pagine-e-file-statici) |
|
||||
| `GET` | qualunque altro percorso (file statici) | nessuna | [API-43](#pagine-e-file-statici) |
|
||||
|
||||
@@ -108,11 +111,15 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
illeggibile, scaduto o con firma errata, un utente non più esistente e un
|
||||
token *superato* (emesso prima dell'ultimo cambio o reset della password)
|
||||
rispondono tutti `401` con codice `session_expired`. L'**assenza** dell'header
|
||||
(o uno schema diverso da `Bearer`) è invece intercettata da FastAPI prima del
|
||||
codice applicativo: `401` con il corpo non strutturato
|
||||
`{"detail": "Not authenticated"}` e header `WWW-Authenticate: Bearer`.
|
||||
(o uno schema diverso da `Bearer`) risponde `401` con codice
|
||||
`not_authenticated`, nel formato strutturato di [API-12], con header
|
||||
`WWW-Authenticate: Bearer`. Il frontend tratta i due codici allo stesso
|
||||
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"}`.
|
||||
|
||||
- **[API-07]** Autenticazione **facoltativa**: senza header, con un header che
|
||||
non inizia con `Bearer `, con un token invalido o superato, la richiesta è
|
||||
@@ -125,11 +132,13 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
- **[API-08]** Autenticazione **admin**: l'header `X-Admin-Token` è confrontato
|
||||
a tempo costante (sui byte UTF-8, così un valore non ASCII non provoca un
|
||||
`500`) con `ADMIN_TOKEN` del `.env`. Un `ADMIN_TOKEN` vuoto nega **tutto**,
|
||||
anche un header vuoto. Il rifiuto è `403` con il corpo non strutturato
|
||||
`{"detail": "invalid admin token"}`. Non c'è identità per singolo operatore
|
||||
anche un header vuoto. Il rifiuto è `403` con codice `invalid_admin_token`,
|
||||
nel formato strutturato di [API-12]. Non c'è identità per singolo operatore
|
||||
(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:
|
||||
- le quattro pagine (`/`, `/admin`, `/guida`, `/report-bug`) rispondono con
|
||||
@@ -137,13 +146,19 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
- il flusso SSE risponde con `Cache-Control: no-store` e
|
||||
`X-Accel-Buffering: no`;
|
||||
- il QR risponde con `Cache-Control: private, max-age=86400, immutable`;
|
||||
- le risposte JSON e i file statici serviti dal mount (`/app.js`,
|
||||
`/style.css`, `/i18n.js`, `/index.html`…) **non** hanno `Cache-Control`:
|
||||
i file statici portano solo `ETag`/`Last-Modified` per la validazione
|
||||
condizionale.
|
||||
- ogni file servito dal mount dei file statici (`/app.js`, `/style.css`,
|
||||
`/i18n.js`, `/admin.js`, `/admin.css`, `/index.html`…) risponde con
|
||||
`Cache-Control: no-cache`, oltre a `ETag`/`Last-Modified`: il browser può
|
||||
tenerne una copia ma la rivalida a ogni uso, così dopo un aggiornamento
|
||||
del server non esegue un `app.js` vecchio contro l'API nuova;
|
||||
- le risposte JSON non hanno `Cache-Control`.
|
||||
*Codice:* `app/main.py:index_page`, `app/main.py:admin_panel`,
|
||||
`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/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.
|
||||
|
||||
- **[API-10]** La documentazione automatica di FastAPI (`/docs`, `/redoc`,
|
||||
`/openapi.json`) è **disattivata** a meno che `ENABLE_API_DOCS` non sia
|
||||
@@ -154,7 +169,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), l'IP è l'**ultimo**
|
||||
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**
|
||||
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).
|
||||
@@ -191,9 +207,20 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
- **[API-13]** Gli errori di `POST /bets` e `POST /withdrawals` sollevati dal
|
||||
servizio rispondono `400`, **tranne** `broadcast_failed`, che risponde `502`:
|
||||
è la rete ad aver rifiutato la transazione, non il chiamante.
|
||||
`broadcast_failed` si usa **solo** per un rifiuto esplicito del server
|
||||
(un errore JSON-RPC in risposta al broadcast), e significa che tutto è stato
|
||||
annullato. Un esito **ambiguo** del broadcast (timeout, connessione caduta
|
||||
dopo l'invio) non è un errore: la richiesta risponde `202 Accepted` con lo
|
||||
stesso corpo del successo e `status: "building"`, perché la transazione può
|
||||
essere già sulla rete e la riga resta al riconciliatore
|
||||
([scommesse-e-prelievi.md](scommesse-e-prelievi.md) B-76). Un rifiuto
|
||||
"già noto / già nel mempool" vale come successo (`201`).
|
||||
*Codice:* `app/api/routes/bets.py:create_bet`,
|
||||
`app/api/routes/withdrawals.py:create_withdrawal` ·
|
||||
*Test:* `tests/unit/test_bets.py`
|
||||
*Stato:* da implementare (B-76) · *Oggi:* qualunque eccezione del broadcast,
|
||||
compresi il timeout di 15 s e la caduta della connessione, annulla tutto e
|
||||
risponde `502 broadcast_failed`.
|
||||
|
||||
- **[API-14]** Qualunque eccezione non gestita risponde `500` con lo stesso
|
||||
formato strutturato, codice `internal_error`, messaggio
|
||||
@@ -204,36 +231,47 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
*Test:* `tests/unit/test_rounds_route.py`
|
||||
|
||||
- **[API-15]** Un corpo, un parametro di percorso o di query che non supera la
|
||||
validazione risponde `422` nel formato **nativo** di FastAPI: `detail` è una
|
||||
**lista** di oggetti `{type, loc, msg, input, ctx?}`, in inglese, uno per
|
||||
campo invalido (anche il JSON malformato arriva così, con
|
||||
`type: "json_invalid"`). Non c'è un `code`: il frontend riconosce la lista e
|
||||
mostra il messaggio generico tradotto `error.invalid_request`.
|
||||
*Codice:* FastAPI (nessun gestore personalizzato),
|
||||
validazione risponde `422` nel formato strutturato di [API-12], con codice
|
||||
`invalid_request`, `message` `"invalid request"` e in `params.errors` la
|
||||
lista degli errori di validazione, uno per campo invalido, ciascuno
|
||||
`{"loc", "msg", "type"}` in inglese (anche il JSON malformato arriva così,
|
||||
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),
|
||||
`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`.
|
||||
|
||||
- **[API-16]** Alcune risposte d'errore **non** seguono il formato strutturato
|
||||
e hanno `detail` come semplice stringa inglese:
|
||||
- **[API-16]** **Nessuna** risposta d'errore ha `detail` come semplice
|
||||
stringa: anche gli errori generati dagli endpoint admin, dal flusso SSE e dal
|
||||
framework seguono il formato strutturato di [API-12], con questi codici:
|
||||
|
||||
| Stato | `detail` | Dove |
|
||||
|---|---|---|
|
||||
| `401` | `"Not authenticated"` | header `Authorization` assente su un endpoint Bearer (FastAPI) |
|
||||
| `403` | `"invalid admin token"` | ogni endpoint `/admin/*` |
|
||||
| `404` | `"user not found"` | `GET /admin/users/{id}/privkey`, `POST /admin/users/{id}/reset-password` |
|
||||
| `404` | `"bug report not found"` | `POST /admin/bug-reports/{id}/status` |
|
||||
| `404` | `"Not Found"` | percorso inesistente (anche un metodo `GET` su un percorso che esiste solo in `POST`, perché lo intercetta il mount dei file statici) |
|
||||
| `405` | `"Method Not Allowed"` | metodo diverso da `GET`/`HEAD` su un percorso non definito da un router |
|
||||
| `503` | `"too many concurrent update streams"` | `GET /rounds/stream` oltre il tetto globale |
|
||||
| Stato | Codice atteso | Dove | Oggi (`detail` stringa) |
|
||||
|---|---|---|---|
|
||||
| `401` | `not_authenticated` | header `Authorization` assente su un endpoint Bearer ([API-06]) | `"Not authenticated"` |
|
||||
| `403` | `invalid_admin_token` | ogni endpoint `/admin/*` ([API-08]) | `"invalid admin token"` |
|
||||
| `404` | `user_not_found` | `GET /admin/users/{id}/privkey`, `POST /admin/users/{id}/reset-password` | `"user not found"` |
|
||||
| `404` | `bug_report_not_found` | `POST /admin/bug-reports/{id}/status` | `"bug report not found"` |
|
||||
| `404` | `not_found` | percorso inesistente (anche un metodo `GET` su un percorso che esiste solo in `POST`, perché lo intercetta il mount dei file statici) | `"Not Found"` |
|
||||
| `405` | `method_not_allowed` | metodo diverso da `GET`/`HEAD` su un percorso non definito da un router | `"Method Not Allowed"` |
|
||||
| `503` | `too_many_streams` | `GET /rounds/stream` oltre il tetto globale | `"too many concurrent update streams"` |
|
||||
|
||||
Il frontend mostra la stringa così com'è.
|
||||
I `404`/`405` del framework si ottengono con un gestore di
|
||||
`StarletteHTTPException` che traduce lo stato nel codice della tabella.
|
||||
*Codice:* `app/api/routes/admin.py:require_admin`,
|
||||
`app/api/routes/admin.py:user_privkey`,
|
||||
`app/api/routes/admin.py:reset_user_password`,
|
||||
`app/api/routes/admin.py:update_bug_report_status`,
|
||||
`app/api/routes/rounds.py:round_stream` ·
|
||||
`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'è.
|
||||
|
||||
- **[API-17]** Il client traduce `code` con la chiave `error.<code>` di
|
||||
`app/static/i18n.js`, passando `params` all'interpolazione e aggiungendo per
|
||||
@@ -267,27 +305,37 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
usato, **senza distinzione tra maiuscole e minuscole**, anche se
|
||||
l'altro account si è registrato nello stesso istante);
|
||||
`409 derivation_index_conflict` (dopo 5 tentativi non è stato possibile
|
||||
assegnare un indice di derivazione libero).
|
||||
assegnare un indice di derivazione libero); `503 registration_suspended`
|
||||
(all'avvio sono stati trovati movimenti sulla catena per indici di
|
||||
derivazione successivi all'ultimo assegnato, cioè il database potrebbe
|
||||
essere un backup più vecchio della realtà; la registrazione resta
|
||||
sospesa finché l'operatore non riconcilia,
|
||||
[operativita.md](operativita.md) B-90 — *da implementare*).
|
||||
- **Limite di frequenza:** al massimo 5 account creati per IP in una finestra
|
||||
mobile di un'ora; la quota è verificata **prima** dell'hash Argon2 e
|
||||
consumata solo da una registrazione riuscita (un nome già preso o un
|
||||
errore di validazione non la consumano). Dettagli in
|
||||
[sicurezza.md](sicurezza.md).
|
||||
- **Effetti:** inserisce l'utente con il nome così come digitato, il
|
||||
prossimo indice di derivazione e l'indirizzo derivato; avvia (senza
|
||||
attenderla, *best effort*) la sottoscrizione Electrum dell'indirizzo. Non
|
||||
scrive audit e non notifica l'SSE. Il flusso completo è in
|
||||
prossimo indice di derivazione e l'indirizzo derivato, e scrive
|
||||
`user_registered` con indice e indirizzo nella stessa transazione; avvia
|
||||
(senza attenderla, *best effort*) la sottoscrizione Electrum
|
||||
dell'indirizzo. Non notifica l'SSE. Il flusso completo è in
|
||||
[registrazione-e-depositi.md](registrazione-e-depositi.md).
|
||||
*Codice:* `app/auth/routes.py:register`, `app/auth/routes.py:RegisterRequest` ·
|
||||
*Test:* `tests/unit/test_auth.py`, `tests/unit/test_users.py`
|
||||
*Stato:* da implementare (B-92, B-90) · *Oggi:* non scrive audit, e non
|
||||
esiste la sospensione `registration_suspended`.
|
||||
|
||||
#### POST /auth/login
|
||||
|
||||
- **[API-19]** Apre una sessione.
|
||||
- **Autenticazione:** nessuna.
|
||||
- **Corpo:** `{"username": stringa, "password": stringa}`, senza vincoli di
|
||||
lunghezza. Lo username è confrontato senza distinzione tra maiuscole e
|
||||
minuscole.
|
||||
- **Corpo:** `{"username": stringa, "password": stringa}`, ciascuno di al
|
||||
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` —
|
||||
@@ -332,20 +380,29 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
|
||||
- **[API-21]** Cambio password self-service.
|
||||
- **Autenticazione:** Bearer.
|
||||
- **Corpo:** `{"current_password": stringa, "new_password": stringa}`.
|
||||
- **Corpo:** `{"current_password": stringa, "new_password": stringa}`,
|
||||
ciascuno di al massimo 256 caratteri.
|
||||
- **Risposta `200`:** `{"access_token": <JWT>}` — un **nuovo** token per la
|
||||
sessione corrente.
|
||||
- **Errori**, nell'ordine in cui sono verificati: `401` di autenticazione;
|
||||
`401 current_password_incorrect`; `400 password_too_short`
|
||||
(`params.minimum` = 8). La lunghezza minima è verificata nel gestore (quindi
|
||||
`400`, non `422` come alla registrazione) e **non** c'è una lunghezza
|
||||
massima.
|
||||
`422 invalid_request` (un campo oltre 256 caratteri); `429 rate_limited`
|
||||
(`params.retry_after_seconds`); `401 current_password_incorrect`;
|
||||
`400 password_too_short` (`params.minimum` = 8). La lunghezza minima è
|
||||
verificata nel gestore (quindi `400`, non `422` come alla registrazione).
|
||||
- **Limite di frequenza:** un contatore di fallimenti di
|
||||
`current_password` per utente, con attesa esponenziale come quello del
|
||||
login; il blocco è verificato prima dell'hash Argon2. Soglie in
|
||||
[sicurezza.md](sicurezza.md) (B-94).
|
||||
- **Effetti:** sostituisce l'hash e incrementa `User.token_version`: ogni
|
||||
token emesso prima, compreso quello usato per questa richiesta, da quel
|
||||
momento riceve `401 session_expired`. Non scrive audit e non notifica
|
||||
l'SSE. Nessun limite di frequenza.
|
||||
*Codice:* `app/api/routes/users.py:change_password` ·
|
||||
momento riceve `401 session_expired`. Scrive `password_changed` nella
|
||||
stessa transazione. Non notifica l'SSE.
|
||||
*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.
|
||||
|
||||
#### GET /users/me/last-round-result
|
||||
|
||||
@@ -376,34 +433,46 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
[scommesse-e-prelievi.md](scommesse-e-prelievi.md).
|
||||
- **Autenticazione:** Bearer.
|
||||
- **Corpo:** nessuno.
|
||||
- **Risposta `201`:**
|
||||
- **Risposta `201`** (il server attivo ha accettato la transazione) o
|
||||
**`202`** (esito del broadcast ambiguo, [API-13](#contratto-degli-errori)):
|
||||
|
||||
| Campo | Tipo | Significato |
|
||||
|---|---|---|
|
||||
| `round_id` | intero | round a cui la scommessa è iscritta |
|
||||
| `bet_txid` | stringa | txid della transazione trasmessa |
|
||||
| `bet_amount_sats` | intero | importo arrivato al montepremi, cioè il costo **meno** la commissione di rete |
|
||||
| `status` | stringa | stato del partecipante, `"broadcast"` |
|
||||
| `bet_txid` | stringa | txid della transazione (trasmessa con `201`, forse trasmessa con `202`) |
|
||||
| `bet_amount_sats` | intero | importo che arriva al montepremi, cioè il costo del round **meno** la commissione di rete |
|
||||
| `status` | stringa | stato del partecipante: `"broadcast"` con `201`, `"building"` con `202` |
|
||||
|
||||
- **Errori:** `503 network_unavailable` (nessuna connessione Electrum,
|
||||
verificato **prima** di ogni altro controllo); poi, tutti `400` salvo
|
||||
indicazione: `no_round_open`, `round_closing`, `already_betting`,
|
||||
`round_full` (`params.max_participants`), `insufficient_balance`
|
||||
(`params.required_sats`), `too_many_inputs` (`params.max_inputs`),
|
||||
`balance_leaves_no_change` (`params.required_extra_sats`),
|
||||
`amount_below_network_fee`, `amount_below_dust_limit`,
|
||||
`502 broadcast_failed`. Il significato di ciascuno è nel
|
||||
`round_full` (`params.max_participants`; anche quando il round ha
|
||||
raggiunto `max_round_pool_sats`, B-95), `balance_pending_confirmation`
|
||||
(`params.pending_sats`; il costo sarebbe coperto dal saldo comprensivo del
|
||||
resto non ancora confermato, ma non da quello confermato, B-96),
|
||||
`insufficient_balance` (`params.required_sats`), `too_many_inputs`
|
||||
(`params.max_inputs`), `balance_leaves_no_change`
|
||||
(`params.required_extra_sats`), `amount_below_network_fee`,
|
||||
`amount_below_dust_limit`, `502 broadcast_failed` (solo rifiuto
|
||||
esplicito). Il significato di ciascuno è nel
|
||||
[catalogo](#catalogo-dei-codici-di-errore).
|
||||
- **Serializzazione:** la richiesta tiene il blocco per-utente condiviso con
|
||||
`POST /withdrawals` per tutta la costruzione e la trasmissione.
|
||||
- **Effetti:** può **aprire un round** se nessuno è attivo e le condizioni
|
||||
lo permettono; segna le monete spese, crea partecipante e transazione in
|
||||
sospeso, trasmette. Audit `bet_placed` in caso di successo,
|
||||
`bet_broadcast_failed` se la rete rifiuta; notifica SSE in entrambi i casi
|
||||
(e all'apertura del round).
|
||||
lo permettono (lo stesso controllo dello scheduler, fatto dentro la
|
||||
richiesta); segna le monete spese, crea partecipante e transazione in
|
||||
sospeso, trasmette al server attivo e poi, *best effort*, a tutti i
|
||||
server di riserva. Audit `bet_placed` in caso di successo,
|
||||
`bet_broadcast_failed` se la rete rifiuta; notifica SSE in tutti e tre i
|
||||
casi (successo, rifiuto, esito ambiguo) e all'apertura del round.
|
||||
*Codice:* `app/api/routes/bets.py:create_bet`,
|
||||
`app/bets/service.py:place_bet` ·
|
||||
*Test:* `tests/unit/test_bets.py`
|
||||
*Stato:* da implementare (B-76, B-95, B-96) · *Oggi:* ogni esito diverso
|
||||
dal successo è `502 broadcast_failed` con tutto annullato; la trasmissione
|
||||
va solo al server attivo; un costo coperto solo da resto non confermato
|
||||
risponde `insufficient_balance`; non c'è tetto al montepremi; il costo è
|
||||
quello configurato al momento della scommessa, non quello del round.
|
||||
|
||||
#### POST /withdrawals
|
||||
|
||||
@@ -417,31 +486,43 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
| `external_address` | stringa | verificato nel servizio (bech32 valido con prefisso `plm1`) |
|
||||
| `amount_sats` | intero | nessun vincolo di schema; il minimo è verificato nel servizio |
|
||||
|
||||
- **Risposta `201`:**
|
||||
- **Risposta `201`** (il server attivo ha accettato la transazione) o
|
||||
**`202`** (esito del broadcast ambiguo, [API-13](#contratto-degli-errori)):
|
||||
|
||||
| Campo | Tipo | Significato |
|
||||
|---|---|---|
|
||||
| `txid` | stringa | txid della transazione trasmessa |
|
||||
| `txid` | stringa | txid della transazione (trasmessa con `201`, forse trasmessa con `202`) |
|
||||
| `amount_requested_sats` | intero | importo chiesto |
|
||||
| `amount_sent_sats` | intero | importo che arriva al destinatario: richiesto meno commissione, e meno quanto serve a lasciare un resto minimo se si preleva tutto |
|
||||
| `status` | stringa | stato del prelievo, `"broadcast"` |
|
||||
| `status` | stringa | stato del prelievo: `"broadcast"` con `201`, `"building"` con `202` |
|
||||
|
||||
- **Errori:** `422`; `503 network_unavailable` (verificato **prima** della
|
||||
validazione dell'indirizzo); poi `400`: `invalid_address`,
|
||||
`withdrawal_to_own_address`, `amount_below_minimum`
|
||||
(`params.minimum_sats` = costo attuale della scommessa),
|
||||
`balance_pending_confirmation` (`params.pending_sats`),
|
||||
`insufficient_balance` (`params.required_sats`), `too_many_inputs`
|
||||
(`params.max_inputs`), `amount_below_network_fee`,
|
||||
`amount_below_dust_limit`; `502 broadcast_failed`.
|
||||
validazione dell'indirizzo, quindi un indirizzo invalido riceve `503`
|
||||
finché la rete non torna); poi `400`: `invalid_address`,
|
||||
`withdrawal_to_own_address`, `withdrawal_to_platform_address`
|
||||
(destinazione uguale a un indirizzo di montepremi, di riserva o delle
|
||||
commissioni, B-80), `amount_below_minimum` (`params.minimum_sats` = costo
|
||||
attuale della scommessa; verificato anche sull'importo ridotto di un
|
||||
prelievo dell'intero saldo, B-96), `balance_pending_confirmation`
|
||||
(`params.pending_sats`), `insufficient_balance` (`params.required_sats`),
|
||||
`too_many_inputs` (`params.max_inputs`), `amount_below_network_fee`,
|
||||
`amount_below_dust_limit`; `502 broadcast_failed` (solo rifiuto
|
||||
esplicito).
|
||||
- **Serializzazione:** stesso blocco per-utente di `POST /bets`.
|
||||
- **Effetti:** segna le monete spese, crea prelievo e transazione in
|
||||
sospeso, trasmette. Audit `withdrawal_sent`, oppure
|
||||
`withdrawal_broadcast_failed` con il prelievo lasciato in stato `failed`;
|
||||
notifica SSE in entrambi i casi.
|
||||
sospeso, trasmette al server attivo e poi, *best effort*, a tutti i
|
||||
server di riserva. Audit `withdrawal_sent`, oppure
|
||||
`withdrawal_broadcast_failed` con il prelievo lasciato in stato `failed`
|
||||
per un rifiuto esplicito; con un esito ambiguo il prelievo resta
|
||||
`building`. Notifica SSE in tutti e tre i casi.
|
||||
*Codice:* `app/api/routes/withdrawals.py:create_withdrawal`,
|
||||
`app/withdrawals/service.py:request_withdrawal` ·
|
||||
*Test:* `tests/unit/test_withdrawals.py`
|
||||
*Stato:* da implementare (B-76, B-80, B-96) · *Oggi:* ogni esito diverso
|
||||
dal successo è `502 broadcast_failed` con il prelievo `failed`; la
|
||||
trasmissione va solo al server attivo; un prelievo verso l'indirizzo del
|
||||
pool o delle commissioni è accettato; l'importo ridotto di un prelievo
|
||||
dell'intero saldo non è riconfrontato con il minimo.
|
||||
|
||||
### Round
|
||||
|
||||
@@ -461,7 +542,7 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
| `opened_at` | stringa ISO o `null` | apertura del round |
|
||||
| `closes_at` | stringa ISO o `null` | scadenza per le scommesse: `opened_at` + la durata **fissata all'apertura del round** (non quella attuale di configurazione) |
|
||||
| `participant_count` | intero | partecipanti **confermati**, cioè quelli tra cui si estrae |
|
||||
| `bet_amount_sats` | intero | costo della scommessa **attualmente configurato** (quello che pagherebbe una nuova scommessa) |
|
||||
| `bet_amount_sats` | intero | costo che pagherebbe una nuova scommessa: con un round attivo il costo **fissato all'apertura del round** (`rounds.bet_amount_sats`), senza round attivo quello configurato — *da implementare (B-95); oggi sempre quello configurato* |
|
||||
| `jackpot_sats` | intero | 70% (arrotondato per difetto) della somma degli importi effettivi delle scommesse confermate; limite superiore, perché la commissione del pagamento è ancora da sottrarre |
|
||||
| `pending_participant_count` | intero | tutti i partecipanti, confermati **e** in volo (totale, non differenza) |
|
||||
| `pending_jackpot_sats` | intero | 70% della somma di tutte le scommesse, confermate e in volo |
|
||||
@@ -470,17 +551,27 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
| `winner_user_id` | intero o `null` | vincitore, valorizzato dall'estrazione in poi (`paying_out`) |
|
||||
| `winner_amount_sats` | intero o `null` | importo al vincitore, valorizzato solo quando la transazione di pagamento è stata costruita |
|
||||
| `draw_block_height` | intero o `null` | altezza del blocco usato per l'estrazione |
|
||||
| `draw_block_hash` | stringa o `null` | hash di quel blocco (il seme dell'estrazione) |
|
||||
| `draw_block_hash` | stringa o `null` | hash di quel blocco; è **uno** degli ingressi del seme, non il seme (vedi [API-45](#get-roundsround_idproof)) |
|
||||
| `draw_waiting_since` | stringa ISO o `null` | inizio dell'attesa del blocco; valorizzato **solo** mentre `status` è `drawing` |
|
||||
| `server_seed_hash` | stringa o `null` | impegno del server sul segreto del round, pubblicato dall'apertura; `null` per un round aperto prima di B-73 — *da implementare (B-73)* |
|
||||
| `participants_hash` | stringa o `null` | impronta dell'elenco ordinato dei partecipanti, valorizzata dall'ingresso in `drawing` — *da implementare (B-73)* |
|
||||
| `draw_height` | intero o `null` | altezza del blocco di estrazione, fissata all'ingresso in `drawing` — *da implementare (B-73)* |
|
||||
| `draw_confirmations` | intero o `null` | conferme richieste sul blocco di estrazione, fissate all'apertura del round — *da implementare (B-73, B-95)* |
|
||||
| `pool_address` | stringa o `null` | indirizzo del montepremi del round, verificabile su un esploratore — *da implementare (B-80)* |
|
||||
| `chain_tip_height` | intero o `null` | ultima altezza nota della catena; `null` finché non è nota |
|
||||
| `lottery_paused` | booleano | pausa di manutenzione attiva |
|
||||
| `lottery_paused` | booleano | lotteria in pausa, per qualunque motivo |
|
||||
| `paused_reason` | stringa o `null` | `operator`, `reorg`, `payout_blocked`, `resurrected_tx`, oppure `null` se non in pausa ([modello-dati.md](modello-dati.md), DAT-69) — *da implementare (B-78, B-82, B-85)* |
|
||||
| `lottery_configured` | booleano | falso finché manca configurazione indispensabile (oggi: `fee_address`), e quindi nessun round può aprirsi |
|
||||
| `user_played` | booleano | vero se il chiamante autenticato ha una riga di partecipazione in questo round, in qualunque stato; sempre falso per un anonimo |
|
||||
|
||||
Il segreto del round (`server_seed`) **non** compare mai in questa
|
||||
risposta: si legge solo da [API-45](#get-roundsround_idproof), dopo che il
|
||||
vincitore è stato scritto.
|
||||
|
||||
Senza round attivo sono valorizzati solo `server_time`, `bet_amount_sats`,
|
||||
`draw_animation_seconds`, `chain_tip_height`, `lottery_paused` e
|
||||
`lottery_configured`; gli altri valgono `null`, `0` o `false`. Il
|
||||
significato di dominio di ogni fase è in
|
||||
`draw_animation_seconds`, `chain_tip_height`, `lottery_paused`,
|
||||
`paused_reason` e `lottery_configured`; gli altri valgono `null`, `0` o
|
||||
`false`. Il significato di dominio di ogni fase è in
|
||||
[ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md).
|
||||
- **Errori:** nessuno previsto (un token invalido non è un errore,
|
||||
[API-07](#convenzioni-generali)).
|
||||
@@ -491,6 +582,9 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
`app/rounds/service.py:round_deadline`, `app/rounds/service.py:winner_share`,
|
||||
`app/rounds/service.py:rounds_can_open` ·
|
||||
*Test:* `tests/unit/test_rounds_route.py`
|
||||
*Stato:* da implementare (B-73, B-78, B-80, B-82, B-85, B-95), per i campi
|
||||
marcati · *Oggi:* quei campi non esistono, `bet_amount_sats` è sempre il
|
||||
costo configurato, e `draw_block_hash` è il seme dell'estrazione.
|
||||
|
||||
#### GET /rounds/stream
|
||||
|
||||
@@ -509,14 +603,88 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
- **Chiusura:** la connessione di un client scomparso senza chiusura pulita
|
||||
è rilevata entro 5 secondi. Oltre **5** flussi contemporanei dallo stesso
|
||||
IP, l'apertura di un nuovo flusso **chiude il più vecchio** di quell'IP.
|
||||
- **Errori:** `503` con `{"detail": "too many concurrent update streams"}`
|
||||
(non strutturato) quando i flussi aperti in totale sono già **500**; il
|
||||
client `EventSource` riprova da solo e intanto resta il polling.
|
||||
- **Errori:** `503 too_many_streams` (formato strutturato,
|
||||
[API-16](#contratto-degli-errori)) quando i flussi aperti in totale sono
|
||||
già **500**; il client `EventSource` riprova da solo e intanto resta il
|
||||
polling.
|
||||
- **Chiusura all'arresto del server:** allo spegnimento ogni flusso aperto
|
||||
viene chiuso dal server, così l'arresto non attende i client
|
||||
([architettura.md](architettura.md) B-93).
|
||||
- **Effetti:** nessuno.
|
||||
- **Cosa non notifica:** `PUT /admin/config`, `POST /admin/pause` e
|
||||
`POST /admin/resume` non pubblicano sul canale; `/` vede
|
||||
`lottery_paused`/`paused_reason` e la nuova configurazione al polling
|
||||
successivo di `GET /rounds/current`.
|
||||
*Codice:* `app/api/routes/rounds.py:round_stream`,
|
||||
`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
|
||||
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.
|
||||
|
||||
#### GET /rounds/{round_id}/proof
|
||||
|
||||
- **[API-45]** La prova pubblica dell'estrazione di un round: tutto ciò che
|
||||
serve a chiunque per ricalcolare il vincitore senza fidarsi del server.
|
||||
L'algoritmo e la codifica esatta dei byte sono definiti in
|
||||
[ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md) (B-73); qui è
|
||||
specificata la forma della risposta.
|
||||
- **Autenticazione:** nessuna.
|
||||
- **Parametro di percorso:** `round_id` intero (non intero: `422`).
|
||||
- **Risposta `200`**, per un round in qualunque stato; i campi non ancora
|
||||
determinati valgono `null` (o lista vuota):
|
||||
|
||||
| Campo | Tipo | Valorizzato da | Significato |
|
||||
|---|---|---|---|
|
||||
| `round_id` | intero | sempre | — |
|
||||
| `status` | stringa | sempre | stato del round |
|
||||
| `algorithm` | stringa | sempre | `"commit-reveal-v1"` per i round aperti con B-73; `"block-hash-v0"` per quelli precedenti, per cui la prova è incompleta (vedi sotto) |
|
||||
| `pool_address` | stringa | sempre | indirizzo del montepremi del round |
|
||||
| `server_seed_hash` | stringa | apertura | `sha256(server_seed)`, in esadecimale |
|
||||
| `server_seed` | stringa o `null` | **solo dopo** che il vincitore è stato scritto (`paying_out` e `closed`) | il segreto del round, 32 byte in esadecimale |
|
||||
| `draw_height` | intero o `null` | ingresso in `drawing` | altezza del blocco di estrazione |
|
||||
| `draw_confirmations` | intero | apertura | conferme richieste su quel blocco prima di leggerne l'hash |
|
||||
| `participants` | lista | ingresso in `drawing` | elenco in ordine di `draw_position`: `{"draw_position", "txid", "vout", "address", "status"}`, dove `txid:vout` è l'output della scommessa verso `pool_address`, `address` l'indirizzo del partecipante (dove andrebbe la vincita) e `status` `confirmed` o `reorged_out` |
|
||||
| `participants_hash` | stringa o `null` | ingresso in `drawing` | impronta dell'elenco `participants` |
|
||||
| `draw_block_hash` | stringa o `null` | estrazione | hash del blocco a `draw_height` |
|
||||
| `seed` | stringa o `null` | estrazione | il seme in decimale (`rounds.seed_int`) |
|
||||
| `winner_index` | intero o `null` | estrazione | `seed mod` numero di elementi di `participants` |
|
||||
| `winner_address` | stringa o `null` | estrazione | `participants[winner_index].address` |
|
||||
|
||||
- **Come si verifica** (round `commit-reveal-v1`):
|
||||
1. prima della chiusura, annotare `server_seed_hash` da `GET /rounds/current`
|
||||
o da questo endpoint;
|
||||
2. dopo la rivelazione, controllare che `sha256(server_seed)` sia uguale a
|
||||
quel valore: il segreto non è stato cambiato dopo l'apertura;
|
||||
3. ricalcolare `participants_hash` da `participants`, e controllare su un
|
||||
esploratore o un nodo che ogni `txid:vout` sia un output confermato
|
||||
verso `pool_address`;
|
||||
4. controllare su un esploratore o un nodo che il blocco a `draw_height`
|
||||
abbia hash `draw_block_hash` e che la catena abbia almeno
|
||||
`draw_confirmations` blocchi da lì;
|
||||
5. ricalcolare `seed` da `server_seed`, `draw_block_hash` e
|
||||
`participants_hash`, poi `winner_index = seed mod n` con `n` il numero
|
||||
di elementi di `participants`, e confrontare `winner_address`.
|
||||
|
||||
Per un round `block-hash-v0` la risposta ha `server_seed_hash`,
|
||||
`server_seed`, `draw_height` e `participants_hash` a `null` e
|
||||
`participants` vuota, perché quei round non hanno registrato né un
|
||||
impegno né l'ordine dei partecipanti: `seed` e `draw_block_hash` si
|
||||
possono confrontare, il vincitore no.
|
||||
- **Riservatezza:** `server_seed` non compare in nessuna risposta, di
|
||||
nessun endpoint (admin compresi), finché `rounds.winner_user_id` è `NULL`
|
||||
([modello-dati.md](modello-dati.md), DAT-64). Gli indirizzi dei
|
||||
partecipanti sono già pubblici sulla catena: le scommesse li collegano a
|
||||
`pool_address`.
|
||||
- **Errori:** `404 round_not_found`; `422`.
|
||||
- **Effetti:** nessuno.
|
||||
*Codice:* `app/api/routes/rounds.py` (endpoint da scrivere),
|
||||
`app/rounds/draw.py:draw_winner` · *Test:* —
|
||||
*Stato:* da implementare (B-73) · *Oggi:* l'endpoint non esiste; l'unico
|
||||
dato pubblico dell'estrazione è `draw_block_hash` in `GET /rounds/current`,
|
||||
finché il round è attivo.
|
||||
|
||||
### QR
|
||||
|
||||
@@ -553,12 +721,19 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
| `contact` | stringa o `null` | facoltativo, al massimo 256 caratteri; spazi rimossi; una stringa vuota diventa `null` |
|
||||
|
||||
- **Risposta `201`:** `{"id": intero}`.
|
||||
- **Errori:** `422`.
|
||||
- **Errori:** `422`; `429 rate_limited` (`params.retry_after_seconds`).
|
||||
- **Limite di frequenza:** una quota di segnalazioni per IP
|
||||
([API-11](#convenzioni-generali)) in una finestra mobile, verificata prima
|
||||
dell'inserimento e consumata solo da un inserimento riuscito, anche per un
|
||||
chiamante autenticato; soglie in [sicurezza.md](sicurezza.md) (B-94).
|
||||
- **Effetti:** inserisce la segnalazione in stato `open`. Nessun audit,
|
||||
nessuna notifica SSE, nessun limite di frequenza.
|
||||
nessuna notifica SSE.
|
||||
*Codice:* `app/api/routes/bug_reports.py:create_bug_report`,
|
||||
`app/api/routes/bug_reports.py:BugReportCreate` ·
|
||||
`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
|
||||
|
||||
@@ -575,7 +750,9 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
### Amministrazione
|
||||
|
||||
Tutti gli endpoint `/admin/*` richiedono `X-Admin-Token`
|
||||
([API-08](#convenzioni-generali)) e rispondono `403` senza di esso. Gli eventi
|
||||
([API-08](#convenzioni-generali)) e rispondono `403 invalid_admin_token`
|
||||
senza di esso. Nessuno restituisce `rounds.server_seed` prima che il
|
||||
vincitore del round sia stato scritto ([API-45](#get-roundsround_idproof)). Gli eventi
|
||||
di audit citati sono descritti in [audit-log.md](audit-log.md); il significato
|
||||
dei parametri operativi in
|
||||
[ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md) e
|
||||
@@ -587,9 +764,13 @@ dei parametri operativi in
|
||||
`round_config`, creata con i valori predefiniti se manca).
|
||||
- **Risposta `200`:** `{"fee_address", "bet_amount_sats",
|
||||
"round_duration_seconds", "round_cooldown_seconds", "fee_rate_sat_vb",
|
||||
"rbf_timeout_seconds", "draw_animation_seconds", "paused"}`.
|
||||
"rbf_timeout_seconds", "draw_animation_seconds", "deposit_confirmations",
|
||||
"draw_confirmations", "max_round_pool_sats", "paused", "paused_reason"}`.
|
||||
*Codice:* `app/api/routes/admin.py:read_config` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-73, B-82, B-85, B-95) · *Oggi:* mancano
|
||||
`deposit_confirmations`, `draw_confirmations`, `max_round_pool_sats` e
|
||||
`paused_reason`.
|
||||
|
||||
#### PUT /admin/config
|
||||
|
||||
@@ -606,41 +787,73 @@ dei parametri operativi in
|
||||
| `fee_rate_sat_vb` | 1 – 10 000 (`MAX_FEE_RATE_SAT_VB`) |
|
||||
| `rbf_timeout_seconds` | 60 – 604 800 |
|
||||
| `draw_animation_seconds` | 0 – 600 |
|
||||
| `deposit_confirmations` | 1 – 100 — *da implementare (B-82)* |
|
||||
| `draw_confirmations` | 1 – 20 — *da implementare (B-73)* |
|
||||
| `max_round_pool_sats` | `0` (disattivato), oppure da `bet_amount_sats` a 4 000 000 000 000 000 — *da implementare (B-95)* |
|
||||
|
||||
`paused` **non** è accettato qui (è ignorato come campo sconosciuto): la
|
||||
I limiti sono quelli di [modello-dati.md](modello-dati.md) (tabella
|
||||
`round_config`), che ne è il proprietario. `paused` e `paused_reason`
|
||||
**non** sono accettati qui (sono ignorati come campi sconosciuti): la
|
||||
pausa ha i suoi endpoint registrati nell'audit.
|
||||
- **Risposta `200`:** la configurazione completa, come
|
||||
[API-30](#get-adminconfig).
|
||||
- **Errori:** `422` per qualunque vincolo violato (nessun campo viene
|
||||
applicato).
|
||||
- **Effetti:** scrive `config_updated` con i valori prima/dopo dei soli
|
||||
campi davvero cambiati; nessuna scrittura se nulla cambia. Durata e pausa
|
||||
tra round si applicano dal round successivo. Nessuna notifica SSE.
|
||||
campi davvero cambiati; nessuna scrittura se nulla cambia. Durata, pausa
|
||||
tra round, costo della scommessa, `fee_address` e `draw_confirmations` si
|
||||
applicano dal round successivo (per costo e `fee_address`: *da
|
||||
implementare*, B-95; oggi subito). Nessuna notifica SSE.
|
||||
*Codice:* `app/api/routes/admin.py:update_config`,
|
||||
`app/api/routes/admin.py:RoundConfigUpdate` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-73, B-82, B-95), per i tre campi nuovi e la
|
||||
copia all'apertura · *Oggi:* sono accettati solo i primi sette campi.
|
||||
|
||||
#### POST /admin/pause e POST /admin/resume
|
||||
|
||||
- **[API-32]** Attivano e disattivano la pausa di manutenzione: impedisce
|
||||
l'apertura del **prossimo** round, il round in corso prosegue fino al
|
||||
pagamento.
|
||||
- **Corpo:** nessuno. **Risposta `200`:** la configurazione completa.
|
||||
- **Effetti:** impostano `paused` a vero/falso e scrivono `lottery_paused` /
|
||||
`lottery_resumed` a **ogni** chiamata, anche se lo stato era già quello.
|
||||
Nessuna notifica SSE: `/` vede `lottery_paused` al polling successivo.
|
||||
- **[API-32]** Attivano e disattivano la pausa: impedisce l'apertura del
|
||||
**prossimo** round, il round in corso prosegue fino al pagamento (salvo un
|
||||
pagamento trattenuto, vedi sotto). La semantica di `paused_reason` è in
|
||||
[modello-dati.md](modello-dati.md) (DAT-69).
|
||||
- **`POST /admin/pause` — corpo:** facoltativo, `{"note": stringa}` di al
|
||||
massimo 256 caratteri, un promemoria libero dell'operatore che finisce solo
|
||||
nel payload di `lottery_paused`. Imposta `paused = true` e
|
||||
`paused_reason = 'operator'`, tranne quando è già attivo un motivo
|
||||
automatico (`reorg`, `payout_blocked`, `resurrected_tx`), che resta.
|
||||
- **`POST /admin/resume` — corpo:** nessuno. È l'unico modo di uscire da
|
||||
**qualunque** pausa, anche automatica: imposta `paused = false`,
|
||||
`paused_reason = NULL` e azzera `rounds.payout_blocked_at` del round
|
||||
attivo, se valorizzato (il pagamento viene poi riverificato,
|
||||
DAT-68). Il payload di `lottery_resumed` riporta il `paused_reason` che è
|
||||
stato tolto, così il registro dice che cosa l'operatore ha accettato di
|
||||
riprendere.
|
||||
- **Risposta `200`** (entrambi): la configurazione completa, come
|
||||
[API-30](#get-adminconfig), compreso `paused_reason`.
|
||||
- **Errori:** `422` (nota oltre 256 caratteri).
|
||||
- **Effetti:** scrivono `lottery_paused` / `lottery_resumed` a **ogni**
|
||||
chiamata, anche se lo stato era già quello. Nessuna notifica SSE: `/`
|
||||
vede `lottery_paused` al polling successivo.
|
||||
*Codice:* `app/api/routes/admin.py:pause_lottery`,
|
||||
`app/api/routes/admin.py:resume_lottery` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-78, B-82, B-85) · *Oggi:* nessun corpo, nessun
|
||||
`paused_reason`, e la pausa è solo manuale.
|
||||
|
||||
#### GET /admin/users
|
||||
|
||||
- **[API-33]** Elenco di **tutti** gli utenti, per `id` crescente, senza
|
||||
paginazione né limite.
|
||||
- **[API-33]** Elenco degli utenti, per `id` crescente, a pagine.
|
||||
- **Query:** `limit` intero 1–500, predefinito 100; `after_id` intero
|
||||
facoltativo: restituisce solo gli utenti con `id` maggiore. La pagina
|
||||
successiva si chiede con `after_id` uguale all'`id` dell'ultimo elemento
|
||||
ricevuto; una lista più corta di `limit` è l'ultima pagina. Fuori
|
||||
intervallo: `422`.
|
||||
- **Risposta `200`:** lista di `{"id", "username", "address",
|
||||
"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
|
||||
|
||||
@@ -649,8 +862,9 @@ dei parametri operativi in
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
- **Parametro:** `user_id` intero.
|
||||
- **Risposta `200`:** `{"address", "wif"}`.
|
||||
- **Errori:** `404` `"user not found"` (non strutturato); `422` se `user_id`
|
||||
non è un intero.
|
||||
- **Errori:** `404 user_not_found` (oggi non strutturato, `"user not
|
||||
found"`, [API-16](#contratto-degli-errori)); `422` se `user_id` non è un
|
||||
intero.
|
||||
- **Effetti:** scrive `admin_privkey_accessed` a ogni accesso.
|
||||
*Codice:* `app/api/routes/admin.py:user_privkey` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
@@ -660,7 +874,8 @@ dei parametri operativi in
|
||||
- **[API-35]** Imposta una nuova password casuale per un utente bloccato.
|
||||
- **Risposta `200`:** `{"username", "new_password"}` — la password (16
|
||||
caratteri URL-safe) è mostrata **solo** in questa risposta.
|
||||
- **Errori:** `404` `"user not found"`; `422`.
|
||||
- **Errori:** `404 user_not_found` (oggi non strutturato,
|
||||
[API-16](#contratto-degli-errori)); `422`.
|
||||
- **Effetti:** sostituisce l'hash, incrementa `token_version` (ogni sessione
|
||||
aperta dell'utente riceve `401 session_expired`), scrive
|
||||
`admin_password_reset`.
|
||||
@@ -674,19 +889,40 @@ dei parametri operativi in
|
||||
- **Risposta `200`:** lista di `{"id", "status", "opened_at", "closed_at",
|
||||
"draw_block_height", "draw_block_hash", "winner_user_id",
|
||||
"winner_username", "pool_amount_sats", "winner_amount_sats",
|
||||
"fee_amount_sats", "payout_txid"}`; i campi non ancora valorizzati sono
|
||||
`null`.
|
||||
"fee_amount_sats", "payout_txid", "pool_address", "server_seed_hash",
|
||||
"draw_height", "bump_reserve_sats", "payout_blocked_at"}`; i campi non
|
||||
ancora valorizzati sono `null`. `server_seed` non è tra i campi: la prova
|
||||
completa di un round è in [API-45](#get-roundsround_idproof).
|
||||
*Codice:* `app/api/routes/admin.py:list_rounds` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-73, B-79, B-80, B-82), per gli ultimi cinque
|
||||
campi · *Oggi:* la lista si ferma a `payout_txid`.
|
||||
|
||||
#### GET /admin/audit-log
|
||||
|
||||
- **[API-37]** Registro di audit, dalla voce più recente.
|
||||
- **Query:** `limit` intero 1–500, predefinito 200.
|
||||
- **[API-37]** Registro di audit, dalla voce più recente, filtrabile e a
|
||||
pagine.
|
||||
- **Query**, tutti facoltativi e combinati in **and**:
|
||||
|
||||
| Parametro | Tipo | Filtro |
|
||||
|---|---|---|
|
||||
| `limit` | intero 1–500, predefinito 200 | numero massimo di voci |
|
||||
| `before_id` | intero | solo voci con `id` minore: la pagina successiva si chiede con l'`id` dell'ultima voce ricevuta; una lista più corta di `limit` è l'ultima pagina |
|
||||
| `event_type` | stringa, ripetibile | solo voci di uno dei tipi indicati, confrontati alla lettera (un tipo inesistente dà una lista vuota) |
|
||||
| `round_id` | intero | solo voci con quel `round_id` |
|
||||
| `user_id` | intero | solo voci con quel `user_id` |
|
||||
| `from` | stringa ISO 8601 con offset | solo voci con `created_at` ≥ questo istante |
|
||||
| `to` | stringa ISO 8601 con offset | solo voci con `created_at` < questo istante |
|
||||
|
||||
Un valore non interpretabile (un intero non valido, un istante senza
|
||||
offset) risponde `422 invalid_request`.
|
||||
- **Risposta `200`:** lista di `{"id", "event_type", "payload" (oggetto),
|
||||
"user_id", "round_id", "created_at"}`.
|
||||
"user_id", "round_id", "created_at"}`, per `id` decrescente.
|
||||
*Codice:* `app/api/routes/admin.py:list_audit_log` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-92), per filtri e paginazione · *Oggi:* accetta
|
||||
solo `limit`, quindi oltre le ultime 500 voci il registro non è
|
||||
consultabile dall'API.
|
||||
|
||||
#### GET /admin/pending-transactions
|
||||
|
||||
@@ -721,7 +957,9 @@ dei parametri operativi in
|
||||
transizione è ammessa, anche all'indietro.
|
||||
- **Risposta `200`:** la segnalazione aggiornata, come
|
||||
[API-39](#get-adminbug-reports).
|
||||
- **Errori:** `422` (stato sconosciuto); `404` `"bug report not found"`.
|
||||
- **Errori:** `422` (stato sconosciuto); `404 bug_report_not_found` (oggi
|
||||
non strutturato, `"bug report not found"`,
|
||||
[API-16](#contratto-degli-errori)).
|
||||
- **Effetti:** scrive `bug_report_status_changed` (con stato precedente e
|
||||
nuovo) solo se lo stato cambia davvero.
|
||||
*Codice:* `app/api/routes/admin.py:update_bug_report_status` ·
|
||||
|
||||
Reference in new issue
Block a user