Files
CRAPP/docs/modules/notifiche.md
T
davideandClaude Opus 5 847972b582 Chiede le credenziali alle route che avvisano tutta la squadra (DD-024).
Le route in src/routes/api/public/ girano con la service role e saltano la RLS,
quindi DD-023 non le copre. Nessuna faceva un controllo di accesso: cercando
"authorization" in quella cartella l'unico header era lo User-Agent con cui
csi.ts chiama il portale CSI. Chiunque conoscesse l'URL poteva far suonare i
telefoni della squadra, e promemoria-palloni accetta perfino una POST con il
corpo vuoto.

La difesa apparente delle altre due — serve un id evento valido — non è una
difesa: l'id è "e" più il timestamp in base 36, compare negli URL che la squadra
si scambia ed è elencabile da qualsiasi utente loggato.

auth-route.server.ts porta i due controlli, diversi perché i chiamanti sono
diversi. apri-sondaggio e sollecita-presenze usano richiediAdmin: token della
sessione verificato con auth.getUser, poi ruolo admin da user_roles, la stessa
fonte di ruoli.ts. Il controllo precede la validazione dell'input, così la
risposta non rivela nemmeno se un evento esiste. promemoria-palloni usa
richiediSegreto, perché la chiama un cron che una sessione non ce l'ha: se
CRON_SEGRETO non è configurata la route resta chiusa con 503, perché una porta
che si riapre da sola quando manca una variabile non se ne accorge nessuno.

csi, push-config, push-subscribe e push-messaggio restano aperte: le chiamano il
browser prima del login e il service worker, dove qualsiasi segreto finirebbe
nel bundle.

Lato client i due pulsanti admin mandano il token con intestazioniAutenticate(),
letto al momento della chiamata e non da uno stato React.

permessi-route.test.ts copre il giro intero — nessun token, giocatore, admin —
avviando il server di sviluppo puntato al database locale, perché servono utenti
veri. Il controllo positivo è il 404: l'admin supera l'accesso e arriva alla
validazione. In api.test.ts restano i rifiuti che non richiedono un utente e
sparisce la verifica della validazione di sollecita-presenze, che ora sta dietro
all'accesso.

I limiti noti di palloni.md sono aggiornati: il secret che il piano originale
prevedeva ora c'è. Resta vero che nessun cron chiama la route, quindi il
promemoria quotidiano non parte da solo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 12:05:39 +02:00

6.3 KiB
Raw Blame History

Modulo — Notifiche

Stato: implementato — un unico opt-in dispositivo abilita tutto il canale push File principali: src/lib/notifiche-smart.ts, src/lib/push-client.ts, src/lib/webpush.server.ts, src/routes/api/public/push-config.ts, src/routes/api/public/push-messaggio.ts, src/routes/api/public/push-subscribe.ts, public/push-sw.js


Obiettivo

Tenere aggiornati i giocatori senza che debbano aprire l'app, con due meccanismi indipendenti:

  • Push VAPID — arrivano anche ad app chiusa (turno palloni, sollecito presenze).
  • Notifiche smart — notifiche locali mostrate solo ad app aperta, generate da badge, serie e obiettivi appena raggiunti; non è un canale push separato.

Dati

push_subscriptions (un dispositivo per riga, chiave endpoint), promemoria_push (coda "consuma e cancella" del testo da mostrare — nonostante il nome, non è uno storico persistente: la riga viene eliminata non appena letta dal service worker).


Iscrizione alle notifiche push

In Profilo → Opzioni c’è un solo interruttore («Notifiche»). Non esistono preferenze separate per tipo di messaggio: liscrizione registra il dispositivo e lo rende destinatario di tutte le push (promemoria palloni, solleciti presenze) e abilita anche le notifiche smart in app, che usano lo stesso service worker.

  1. Il giocatore attiva «Notifiche» in /profilo → richiesta permesso browser.
  2. GET /api/public/push-config restituisce solo la chiave pubblica VAPID.
  3. Registrazione del service worker public/push-sw.js e pushManager.subscribe().
  4. POST /api/public/push-subscribe registra endpoint e chiavi in push_subscriptions (upsert).

Ruolo delle tre route pubbliche

  • push-config — espone la sola chiave pubblica VAPID.
  • push-subscribe — registra o rimuove l'iscrizione di un dispositivo.
  • apri-sondaggio — premuto da un admin dalla pagina partita: mette in coda su promemoria_push l'avviso di apertura del sondaggio pre-partita per tutti i dispositivi iscritti e manda la push (vedi Scout Live).
  • push-messaggio — non invia nulla: il service worker la interroga al momento della ricezione di una push (che arriva sempre "vuota", senza testo, per compatibilità) per sapere quale messaggio mostrare. Priorità: un messaggio in coda su promemoria_push (scritto da sollecita-presenze, vedi Presenze), altrimenti il messaggio calcolato al volo sul turno palloni (vedi Palloni).

L'invio effettivo (src/lib/webpush.server.ts, funzione inviaPush) firma un JWT VAPID (ECDSA P-256) e fa una POST senza corpo all'endpoint push del browser; è riusato identico da sollecita-presenze.ts e promemoria-palloni.ts.

Chi può farle partire (DD-024)

Queste route usano la service role e saltano la RLS, quindi il permesso deve stare nella route. src/lib/auth-route.server.ts fornisce i due controlli:

Route Controllo Chi la chiama
apri-sondaggio, sollecita-presenze richiediAdmin — token della sessione Supabase, poi ruolo admin in user_roles l'app, dal pulsante riservato agli admin
promemoria-palloni richiediSegreto — intestazione x-cron-segreto uguale alla variabile CRON_SEGRETO un cron, senza sessione
csi, push-config, push-subscribe, push-messaggio nessuno il browser prima del login e il service worker, che una sessione non ce l'hanno

CRON_SEGRETO va configurata negli ambienti: se manca, promemoria-palloni risponde 503 e il promemoria non parte. È voluto — una porta che si riapre da sola quando manca una configurazione non se ne accorge nessuno.


Notifiche smart

calcolaNotifiche() (notifiche-smart.ts) genera un evento solo quando "c'è qualcosa di reale": badge appena sbloccato, "sei a un passo" da un traguardo, serie che raggiunge un traguardo esatto, obiettivo di squadra tra il 90 e il 100%, badge social vinto. Ogni notifica ha un id deterministico; quelli già mostrati sono salvati in localStorage per non ripetersi — deduplica puramente locale al dispositivo, non sincronizzata.


Limiti noti

  • Non ci sono preferenze granulari (solo palloni / solo presenze / solo smart): un dispositivo è iscritto o no. Separare i canali richiederebbe schema e UI dedicati.
  • promemoria_push è descritta altrove come "storico" ma nel codice è una coda che si autocancella alla lettura: non conserva nulla.
  • Nessuna verifica di autenticazione su push-messaggio: chiunque conosca un endpoint push valido può leggerne il messaggio. Non è chiudibile con un segreto, perché a chiamarla è il service worker, dove qualsiasi segreto sarebbe pubblico; di fatto la protegge il dover conoscere l'endpoint, che è un URL segreto per dispositivo. promemoria-palloni invece è chiusa da DD-024.
  • Compatibilità iOS/Safari non gestita esplicitamente nel codice (nessun branch dedicato): serve l'installazione da schermata Home per funzionare, ma l'app non lo segnala esplicitamente.
  • Le notifiche smart dipendono da un service worker già registrato: se il giocatore non ha mai attivato le push, notificaSistema() non ha un reg a cui appoggiarsi e la notifica locale non viene mai mostrata, anche con permesso concesso.
  • Payload push sempre vuoto: ogni notifica richiede una fetch aggiuntiva (push-messaggio) per ottenere il testo, quindi serve rete disponibile anche solo per mostrare il messaggio.

Evoluzioni possibili

  • Preferenze per canale (palloni, solleciti, smart), se servono davvero alla squadra.
  • Aggiungere autenticazione alle route pubbliche coinvolte.
  • Gestire esplicitamente il caso iOS (messaggio se l'app non è installata da Home).