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>
73 lines
2.3 KiB
TypeScript
73 lines
2.3 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import type { Session } from "@supabase/supabase-js";
|
|
import { supabase } from "@/integrations/supabase/client";
|
|
|
|
/**
|
|
* Autenticazione reale con Google (DD-011). Il login ha sostituito la selezione del
|
|
* giocatore: senza sessione non si entra, e i permessi di amministrazione arrivano solo
|
|
* da `user_roles` (vedi `ruoli.ts`).
|
|
*/
|
|
export function useSessione() {
|
|
const [sessione, setSessione] = useState<Session | null>(null);
|
|
const [pronta, setPronta] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let attivo = true;
|
|
// Il client Supabase esplode alla costruzione se mancano le variabili d'ambiente:
|
|
// qui va assorbito, altrimenti la schermata di accesso non si disegna proprio e
|
|
// resta irraggiungibile anche la selezione del giocatore.
|
|
try {
|
|
supabase.auth
|
|
.getSession()
|
|
.then(({ data }) => {
|
|
if (!attivo) return;
|
|
setSessione(data.session);
|
|
setPronta(true);
|
|
})
|
|
.catch(() => attivo && setPronta(true));
|
|
const { data } = supabase.auth.onAuthStateChange((_evento, nuova) => setSessione(nuova));
|
|
return () => {
|
|
attivo = false;
|
|
data.subscription.unsubscribe();
|
|
};
|
|
} catch (errore) {
|
|
console.error("[auth] Supabase non disponibile", errore);
|
|
setPronta(true);
|
|
return () => {
|
|
attivo = false;
|
|
};
|
|
}
|
|
}, []);
|
|
|
|
return {
|
|
sessione,
|
|
pronta,
|
|
utenteId: sessione?.user.id ?? null,
|
|
emailUtente: sessione?.user.email ?? null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Intestazioni con il token della sessione, per le route server che verificano il ruolo
|
|
* (DD-024). Letta al momento della chiamata e non da uno stato React, così non si spedisce
|
|
* un token già scaduto.
|
|
*/
|
|
export async function intestazioniAutenticate(): Promise<Record<string, string>> {
|
|
const { data } = await supabase.auth.getSession();
|
|
const token = data.session?.access_token;
|
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
}
|
|
|
|
export async function accediConGoogle(): Promise<void> {
|
|
const { error } = await supabase.auth.signInWithOAuth({
|
|
provider: "google",
|
|
options: { redirectTo: window.location.origin },
|
|
});
|
|
if (error) throw error;
|
|
}
|
|
|
|
export async function esci(): Promise<void> {
|
|
const { error } = await supabase.auth.signOut();
|
|
if (error) throw error;
|
|
}
|