Il promemoria palloni lo manda un admin dalla pagina evento (DD-025).

La route era disegnata per un cron quotidiano: calcolava chi è di turno oggi e
gli mandava una push. Ma nel repository nessun cron esiste, e palloni.md lo
annotava già come "da verificare lato hosting": nei fatti quel promemoria non è
mai partito. Il segreto condiviso introdotto ieri proteggeva una porta che
nessuno apriva, al prezzo di una variabile d'ambiente da configurare ovunque.

Ora la fa partire un amministratore dal pulsante "Avvisa chi è di turno", dentro
il riquadro palloni dell'evento. La route accetta un eventoId e avvisa i
destinatari di quell'evento invece della giornata corrente: chi deve prendere i
palloni e chi deve riportarli, con un testo diverso per ciascuno. Stesso
precedente di apri-sondaggio, manuale fin dalla v1.0.6.

Il testo va in coda su promemoria_push prima dell'invio. Serve: la push parte
vuota e il service worker chiede a push-messaggio cosa mostrare, ma quella route
sa raccontare solo la giornata corrente, quindi un avviso mandato il martedì per
il sabato arriverebbe con il testo generico.

avvisiPalloniEvento() è la nuova funzione pura, con i suoi test: due destinatari
con testi diversi, un avviso solo quando sono la stessa persona, niente avvisi
per un evento inesistente o senza turni. Un'asserzione verifica che il testo non
dica mai "oggi", perché può arrivare giorni prima.

richiediSegreto e CRON_SEGRETO spariscono: tutte e tre le route di notifica
usano ora richiediAdmin, e non resta nessuna variabile da configurare.
destinatariPromemoriaPalloni() resta in palloni-core.ts con i suoi test perché
push-messaggio continua a usarla per il testo calcolato al volo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 12:15:39 +02:00
co-authored by Claude Opus 5
parent 847972b582
commit ee9f3f1b8c
10 changed files with 260 additions and 96 deletions
+6 -6
View File
@@ -117,18 +117,18 @@ try {
// Il controllo di accesso viene prima della validazione: senza credenziali la
// risposta non deve nemmeno dire se l'evento esiste.
await prova("le route che avvisano la squadra chiedono le credenziali", async () => {
for (const percorso of ["/api/public/sollecita-presenze", "/api/public/apri-sondaggio"]) {
const route = [
"/api/public/sollecita-presenze",
"/api/public/apri-sondaggio",
"/api/public/promemoria-palloni",
];
for (const percorso of route) {
assert.equal(
(await postJson(percorso, { eventoId: "non-esiste" })).status,
401,
`${percorso} senza token`,
);
}
assert.equal(
(await postJson("/api/public/promemoria-palloni", {})).status < 400,
false,
"promemoria-palloni senza segreto non parte",
);
});
await prova("un token malformato non passa", async () => {
+10 -25
View File
@@ -22,13 +22,11 @@ if (!locale) {
riepilogo("permessi route");
} else {
const { url: SUPABASE, anon: ANON, servizio: SERVIZIO } = locale;
const SEGRETO = "segreto-di-prova";
// Il server di sviluppo eredita queste: le route leggono i nomi senza prefisso.
process.env["SUPABASE_URL"] = SUPABASE;
process.env["SUPABASE_PUBLISHABLE_KEY"] = ANON;
process.env["SUPABASE_SERVICE_ROLE_KEY"] = SERVIZIO;
process.env["CRON_SEGRETO"] = SEGRETO;
const PASSWORD = "prova-route-123";
const idUtenti: string[] = [];
@@ -74,6 +72,13 @@ if (!locale) {
const server = await avviaServer();
console.log(`permessi route su ${server.baseUrl} (database ${SUPABASE})`);
/** Le tre route che mandano notifiche: stesso controllo, stesso corpo (DD-024, DD-025). */
const ROUTE = [
"/api/public/sollecita-presenze",
"/api/public/apri-sondaggio",
"/api/public/promemoria-palloni",
];
const chiama = (percorso: string, intestazioni: Record<string, string> = {}) =>
fetch(`${server.baseUrl}${percorso}`, {
method: "POST",
@@ -83,12 +88,11 @@ if (!locale) {
try {
await prova("senza token la route non risponde nemmeno se l'evento esiste", async () => {
assert.equal((await chiama("/api/public/sollecita-presenze")).status, 401);
assert.equal((await chiama("/api/public/apri-sondaggio")).status, 401);
for (const percorso of ROUTE) assert.equal((await chiama(percorso)).status, 401, percorso);
});
await prova("un giocatore autenticato non avvisa la squadra", async () => {
for (const percorso of ["/api/public/sollecita-presenze", "/api/public/apri-sondaggio"]) {
for (const percorso of ROUTE) {
const res = await chiama(percorso, { authorization: `Bearer ${tokenGiocatore}` });
assert.equal(res.status, 403, `${percorso}: token valido ma senza ruolo`);
}
@@ -98,30 +102,11 @@ if (!locale) {
// validazione dell'input. Il 404 dice esattamente questo — è passato, e l'evento
// inventato non esiste.
await prova("un amministratore passa e arriva alla validazione", async () => {
for (const percorso of ["/api/public/sollecita-presenze", "/api/public/apri-sondaggio"]) {
for (const percorso of ROUTE) {
const res = await chiama(percorso, { authorization: `Bearer ${tokenAdmin}` });
assert.equal(res.status, 404, `${percorso}: superato l'accesso, evento inesistente`);
}
});
await prova("il promemoria palloni chiede il segreto del cron", async () => {
const senza = await fetch(`${server.baseUrl}/api/public/promemoria-palloni`, {
method: "POST",
});
assert.equal(senza.status, 401, "senza segreto non parte");
const sbagliato = await fetch(`${server.baseUrl}/api/public/promemoria-palloni`, {
method: "POST",
headers: { "x-cron-segreto": "non-e-questo" },
});
assert.equal(sbagliato.status, 401, "un segreto sbagliato vale come nessun segreto");
const giusto = await fetch(`${server.baseUrl}/api/public/promemoria-palloni`, {
method: "POST",
headers: { "x-cron-segreto": SEGRETO },
});
assert.equal(giusto.status, 200, "con il segreto giusto il job parte");
});
} finally {
server.stop();
for (const id of idUtenti) {
+37
View File
@@ -5,6 +5,7 @@ import type { Evento } from "@/lib/eventi";
import {
completaTurni,
conteggioTurni,
avvisiPalloniEvento,
destinatariPromemoriaPalloni,
eventiDelGiorno,
eventiPalloni,
@@ -139,6 +140,42 @@ assert.deepEqual(
"stessa persona oggi e alla volta prima: un solo avviso",
);
// --- avvisiPalloniEvento ------------------------------------------------------
// Il promemoria per un evento scelto dall'admin: due destinatari, con testi diversi.
const avvisi = avvisiPalloniEvento(turniPush, eventiPush, "p2");
assert.deepEqual(
avvisi.map((a) => a.giocatoreId),
["g2", "g1"],
"l'incaricato di questo evento, e chi ha i palloni dalla volta prima",
);
assert.match(avvisi[0]!.titolo, /prendere i palloni/);
assert.match(avvisi[1]!.titolo, /Porta i palloni/);
assert.ok(
avvisi.every((a) => !/oggi/i.test(a.testo)),
"il testo nomina l'evento, non 'oggi': può arrivare giorni prima",
);
assert.deepEqual(
avvisiPalloniEvento(turniPush, eventiPush, "p1").map((a) => a.giocatoreId),
["g1"],
"il primo evento non ha un precedente da avvisare",
);
assert.deepEqual(
avvisiPalloniEvento({ p1: "g1", p2: "g1" }, eventiPush, "p2").map((a) => a.giocatoreId),
["g1"],
"stessa persona nei due ruoli: un avviso solo, non due notifiche uguali",
);
assert.deepEqual(
avvisiPalloniEvento(turniPush, eventiPush, "non-esiste"),
[],
"un evento inesistente non produce avvisi",
);
assert.deepEqual(
avvisiPalloniEvento({}, eventiPush, "p2"),
[],
"senza turni assegnati non c'è nessuno da avvisare",
);
// --- messaggioPalloniOggi -----------------------------------------------------
assert.deepEqual(
messaggioPalloniOggi(turniPush, eventiPush, "2026-02-02", "g1", "Mario"),