Aggiunge copertura test e documentazione per Notifiche attive in admin
Estrae la deduplica degli id giocatore in idsConNotificheAttive (src/lib/notifiche-attive.server.ts), testata a livello unit. Aggiunge un test di integrazione sui permessi della route (401/403/200), sullo stesso schema di permessi-route.test.ts. Documenta il nuovo endpoint in notifiche.md e la dashboard admin a tab in profilo-giocatore.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,10 +81,14 @@ route. Tutte e tre partono da un gesto di un amministratore dentro l'app, quindi
|
|||||||
è uno solo (`richiediAdmin` in `src/lib/auth-route.server.ts`) e non serve configurare nessuna
|
è uno solo (`richiediAdmin` in `src/lib/auth-route.server.ts`) e non serve configurare nessuna
|
||||||
variabile d'ambiente.
|
variabile d'ambiente.
|
||||||
|
|
||||||
| Route | Controllo | Chi la chiama |
|
| Route | Controllo | Chi la chiama |
|
||||||
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
| ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||||
| `apri-sondaggio`, `sollecita-presenze`, `promemoria-palloni` | `richiediAdmin` — token della sessione Supabase, poi ruolo `admin` in `user_roles` | l'app, da un pulsante riservato agli admin |
|
| `apri-sondaggio`, `sollecita-presenze`, `promemoria-palloni`, `notifiche-attive` | `richiediAdmin` — token della sessione Supabase, poi ruolo `admin` in `user_roles` | l'app, da un pulsante o una vista riservati agli admin |
|
||||||
| `csi`, `push-config`, `push-subscribe` | nessuno | il browser prima del login, che una sessione non ce l'ha ancora |
|
| `csi`, `push-config`, `push-subscribe` | nessuno | il browser prima del login, che una sessione non ce l'ha ancora |
|
||||||
|
|
||||||
|
`notifiche-attive` è a sola lettura: non manda push, restituisce gli id giocatore con almeno
|
||||||
|
un dispositivo iscritto in `push_subscriptions` (deduplicati). Alimenta la tab "Notifiche"
|
||||||
|
della dashboard admin (vedi [Profilo giocatore](profilo-giocatore.md)), non l'invio effettivo.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -165,9 +165,17 @@ Contiene.
|
|||||||
## Dashboard amministratore
|
## Dashboard amministratore
|
||||||
|
|
||||||
Gli amministratori dispongono di una schermata dedicata (`/admin`, raggiungibile da
|
Gli amministratori dispongono di una schermata dedicata (`/admin`, raggiungibile da
|
||||||
Profilo → Opzioni).
|
Profilo → Opzioni), organizzata in tab scorrevoli a pillole (`BarraSottosezioni`, stesso
|
||||||
|
componente di [Squadra](squadra.md) e Campionato): Squadra, Profili, Disattivati (solo se
|
||||||
|
c'è almeno un giocatore disattivato) e Notifiche.
|
||||||
|
|
||||||
Per ogni giocatore vengono mostrati.
|
La tab **Notifiche** mostra quanti giocatori attivi hanno almeno un dispositivo iscritto
|
||||||
|
alle notifiche push e i loro nomi, leggendo `GET /api/public/notifiche-attive` (vedi
|
||||||
|
[Notifiche](notifiche.md)). È solo consultiva: l'attivazione resta un gesto che ogni
|
||||||
|
giocatore deve fare dal proprio dispositivo (Profilo), l'admin non può attivarla per conto
|
||||||
|
di altri.
|
||||||
|
|
||||||
|
Per ogni giocatore, nella tab Profili, vengono mostrati.
|
||||||
|
|
||||||
- Stato del profilo
|
- Stato del profilo
|
||||||
- Certificato medico
|
- Certificato medico
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/** Id giocatore distinti tra le righe di `push_subscriptions` (una riga per dispositivo). */
|
||||||
|
export function idsConNotificheAttive(righe: Array<{ giocatore_id: string }>): string[] {
|
||||||
|
return [...new Set(righe.map((r) => r.giocatore_id))];
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { richiediAdmin } from "@/lib/auth-route.server";
|
import { richiediAdmin } from "@/lib/auth-route.server";
|
||||||
|
import { idsConNotificheAttive } from "@/lib/notifiche-attive.server";
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/public/notifiche-attive")({
|
export const Route = createFileRoute("/api/public/notifiche-attive")({
|
||||||
server: {
|
server: {
|
||||||
@@ -17,8 +18,7 @@ export const Route = createFileRoute("/api/public/notifiche-attive")({
|
|||||||
return new Response("Errore lettura", { status: 500 });
|
return new Response("Errore lettura", { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const giocatoreIds = [...new Set((data ?? []).map((r) => r.giocatore_id))];
|
return Response.json({ giocatoreIds: idsConNotificheAttive(data ?? []) });
|
||||||
return Response.json({ giocatoreIds });
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* Chi può leggere l'elenco dei giocatori con notifiche push attive:
|
||||||
|
* `bun test/integration/notifiche-attive-route.test.ts`.
|
||||||
|
*
|
||||||
|
* `/api/public/notifiche-attive` legge `push_subscriptions` con la service role e
|
||||||
|
* salta la RLS (come le route che mandano notifiche, DD-024): il permesso deve stare
|
||||||
|
* nella route. Serve un database vero per provare token di un giocatore normale e di
|
||||||
|
* un amministratore, quindi solo stack locale.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { statoLocale } from "../helpers/locale";
|
||||||
|
import { avviaServer, json } from "../helpers/server";
|
||||||
|
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||||
|
|
||||||
|
const locale = statoLocale();
|
||||||
|
|
||||||
|
if (!locale) {
|
||||||
|
salta("notifiche attive: permessi route", "stack locale non attivo (npx supabase start)");
|
||||||
|
riepilogo("notifiche-attive-route");
|
||||||
|
} else {
|
||||||
|
const { url: SUPABASE, anon: ANON, servizio: SERVIZIO } = locale;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
const PASSWORD = "prova-notifiche-123";
|
||||||
|
const idUtenti: string[] = [];
|
||||||
|
|
||||||
|
const authAdmin = { apikey: SERVIZIO, Authorization: `Bearer ${SERVIZIO}` };
|
||||||
|
|
||||||
|
async function creaUtente(email: string): Promise<string> {
|
||||||
|
const res = await fetch(`${SUPABASE}/auth/v1/admin/users`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...authAdmin, "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ email, password: PASSWORD, email_confirm: true }),
|
||||||
|
});
|
||||||
|
const corpo = (await res.json()) as { id?: string };
|
||||||
|
if (!corpo.id) throw new Error(`creazione utente fallita: ${JSON.stringify(corpo)}`);
|
||||||
|
idUtenti.push(corpo.id);
|
||||||
|
return corpo.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function accedi(email: string): Promise<string> {
|
||||||
|
const res = await fetch(`${SUPABASE}/auth/v1/token?grant_type=password`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { apikey: ANON, "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ email, password: PASSWORD }),
|
||||||
|
});
|
||||||
|
const corpo = (await res.json()) as { access_token?: string };
|
||||||
|
if (!corpo.access_token) throw new Error(`accesso fallito: ${JSON.stringify(corpo)}`);
|
||||||
|
return corpo.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailGiocatore = `test-notifiche-giocatore-${Date.now()}@example.test`;
|
||||||
|
const emailAdmin = `test-notifiche-admin-${Date.now()}@example.test`;
|
||||||
|
await creaUtente(emailGiocatore);
|
||||||
|
const idAdmin = await creaUtente(emailAdmin);
|
||||||
|
await fetch(`${SUPABASE}/rest/v1/user_roles`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...authAdmin, "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ user_id: idAdmin, role: "admin" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenGiocatore = await accedi(emailGiocatore);
|
||||||
|
const tokenAdmin = await accedi(emailAdmin);
|
||||||
|
|
||||||
|
const server = await avviaServer();
|
||||||
|
console.log(`notifiche-attive-route su ${server.baseUrl} (database ${SUPABASE})`);
|
||||||
|
|
||||||
|
const PERCORSO = "/api/public/notifiche-attive";
|
||||||
|
const chiama = (intestazioni: Record<string, string> = {}) =>
|
||||||
|
fetch(`${server.baseUrl}${PERCORSO}`, { headers: intestazioni });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prova("senza token la route non risponde", async () => {
|
||||||
|
assert.equal((await chiama()).status, 401);
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova("un giocatore autenticato non è admin", async () => {
|
||||||
|
const res = await chiama({ authorization: `Bearer ${tokenGiocatore}` });
|
||||||
|
assert.equal(res.status, 403);
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova("un amministratore riceve l'elenco degli id", async () => {
|
||||||
|
const res = await chiama({ authorization: `Bearer ${tokenAdmin}` });
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
const corpo = (await json(res)) as { giocatoreIds: unknown };
|
||||||
|
assert.ok(Array.isArray(corpo.giocatoreIds), "giocatoreIds è un array");
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
server.stop();
|
||||||
|
for (const id of idUtenti) {
|
||||||
|
await fetch(`${SUPABASE}/rest/v1/user_roles?user_id=eq.${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: authAdmin,
|
||||||
|
});
|
||||||
|
await fetch(`${SUPABASE}/auth/v1/admin/users/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: authAdmin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
riepilogo("notifiche-attive-route");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Check della deduplica per la sezione Notifiche in admin:
|
||||||
|
* `bun test/unit/notifiche-attive.test.ts`.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { idsConNotificheAttive } from "@/lib/notifiche-attive.server";
|
||||||
|
|
||||||
|
assert.deepEqual(idsConNotificheAttive([]), [], "nessuna riga -> nessun id");
|
||||||
|
|
||||||
|
assert.deepEqual(idsConNotificheAttive([{ giocatore_id: "g1" }]), ["g1"], "una riga -> un id");
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
idsConNotificheAttive([{ giocatore_id: "g1" }, { giocatore_id: "g1" }, { giocatore_id: "g2" }]),
|
||||||
|
["g1", "g2"],
|
||||||
|
"più dispositivi dello stesso giocatore contano una volta sola",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
idsConNotificheAttive([{ giocatore_id: "g2" }, { giocatore_id: "g1" }]),
|
||||||
|
["g2", "g1"],
|
||||||
|
"ordine di prima comparsa, non alfabetico",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("notifiche-attive: ok");
|
||||||
Reference in New Issue
Block a user