Sistema di Autenticazione β
π Panoramica β
Il sistema di autenticazione usa clientLoader di React Router per proteggere le route e Zustand per la gestione dello stato. Ogni route protetta ha una singola riga di codice che blocca l'accesso prima del render.
ποΈ Architettura β
Il sistema Γ¨ composto da:
- Zustand Store (
app/store/auth/auth.store.ts) - Gestione centralizzata dello stato con persist su localStorage - Auth Service (
app/lib/auth/auth.ts) - Logica di business per login/logout - Loaders (
app/lib/auth/loaders.ts) - Factory functions per proteggere le route - API Layer (
app/api/auth/auth.api.ts) - Chiamate HTTP al backend - Schemas (
app/schemas/auth/auth.schema.ts) - Validazione con Zod
π§ Configurazione β
Le route sono protette individualmente usando factory functions:
// app/routes/dashboard.tsx
import { createProtectedLoader } from "@/lib/auth/loaders";
// β
Una riga per proteggere la route
export const clientLoader = createProtectedLoader();
// β
Protezione con verifica settings utente
export const clientLoader = createProtectedLoader({ requireSettings: true });
// β
Protezione con caricamento settings (solo per /welcome)
export const clientLoader = createProtectedLoader({ loadSettings: true });
export default function Dashboard() {
return <div>Contenuto protetto</div>;
}Factory Functions Disponibili β
createProtectedLoader(options?): Per route che richiedono autenticazione- Senza opzioni: verifica solo l'autenticazione
options.requireSettings: Setrue, verifica sia l'autenticazione che la presenza dei settings utente. Se i settings non sono presenti, reindirizza a/welcomedove vengono caricati. Utile per route che dipendono dai dati di configurazione utente (azienda, moduli, preferenze).options.loadSettings: Setrue, carica i settings PRIMA del render (previene flash). Usare solo nella pagina/welcome.
createGuestLoader(): Per route accessibili solo agli utenti non autenticati (es. login)
π‘οΈ Proteggere una Pagina β
Step 1: Registra la Route β
Aggiungi la route in app/routes.ts:
export default [
route("settings", "routes/settings.tsx"),
] satisfies RouteConfig;Step 2: Aggiungi il clientLoader β
Crea il file della route con protezione:
// app/routes/settings.tsx
import { AuthenticatedLayout } from "@/components/layout/authenticated-layout";
import { createProtectedLoader } from "@/lib/auth/loaders";
// β
Protezione base (solo autenticazione)
export const clientLoader = createProtectedLoader();
// β
Protezione con verifica settings (se la pagina necessita dei dati utente)
// requireSettings: true verifica SIA l'autenticazione CHE la presenza dei settings
// Se i settings non sono presenti, reindirizza a /welcome dove vengono caricati
export const clientLoader = createProtectedLoader({ requireSettings: true });
export default function Settings() {
return (
<AuthenticatedLayout>
<h1>Impostazioni</h1>
{/* Il tuo contenuto */}
</AuthenticatedLayout>
);
}La pagina Γ¨ protetta senza flash di contenuto!
π‘ Quando usare
requireSettings: true?
- Usa questa opzione quando la pagina necessita dei dati utente (azienda, moduli, preferenze)
requireSettings: trueverifica sia l'autenticazione che la presenza dei settings- Se l'utente non Γ¨ autenticato β reindirizza al login
- Se i settings non sono stati ancora caricati β reindirizza a
/welcome- La pagina
/welcomeusaloadSettings: trueper caricare i settings PRIMA del render- Questo garantisce zero flash di contenuto e dati sempre disponibili
π‘ PerchΓ© serve
clientLoader? IlclientLoaderè l'unico modo per evitare il flash di contenuto non autorizzato in SPA mode. Viene eseguito PRIMA del render del componente, quindi:
- Se l'utente non Γ¨ autenticato β reindirizza al login PRIMA del render
- Se servono i settings e non ci sono β reindirizza a
/welcomePRIMA del render- Se
loadSettings: trueβ carica i settings PRIMA del render (blocca finchΓ© non arrivano)## π Creare una Pagina Pubblica (Guest-Only)
Per pagine come login o registrazione che devono reindirizzare gli utenti giΓ autenticati:
Step 1: Registra la Route β
Aggiungi la route in app/routes.ts:
export default [
route("register", "routes/register.tsx"),
] satisfies RouteConfig;Step 2: Aggiungi il clientLoader β
Crea il file con protezione guest-only:
// app/routes/register.tsx
import { createGuestLoader } from "@/lib/auth/loaders";
// β
Reindirizza utenti autenticati PRIMA del render
export const clientLoader = createGuestLoader();
export default function Register() {
return (
<div>
<h1>Registrazione</h1>
{/* Form di registrazione */}
</div>
);
}Gli utenti autenticati vengono automaticamente reindirizzati a /welcome!
π Route Pubbliche Generiche β
Per pagine accessibili a tutti (autenticati e non), semplicemente non aggiungere il clientLoader:
// app/routes/about.tsx
export default function About() {
return (
<div>
<h1>Chi Siamo</h1>
<p>Questa pagina Γ¨ accessibile a tutti.</p>
</div>
);
}Nessun clientLoader = accessibile a tutti!
π Sincronizzazione Multi-Tab β
Il sistema sincronizza automaticamente lo stato di autenticazione tra tutte le schede/finestre del browser usando:
1. Zustand Persist (localStorage events) β
- β Sincronizzazione automatica dello stato tra tab
- β Persiste isAuthenticated, loginData e userSettings
- β Ripristina lo stato al reload della pagina
2. BroadcastChannel API (navigazione sincronizzata) β
- β Logout su una tab β logout istantaneo su tutte le tab + navigazione a login
- β Login su una tab β sincronizzazione automatica + navigazione appropriata
- β Selezione azienda β tutte le tab navigano al modulo di default o /welcome
- β Ritorno alla lista aziende β tutte le tab navigano a /companies
- β PiΓΉ veloce e affidabile di storage events per la navigazione
- β Supporta dati complessi (non solo stringhe)
Il componente AuthSyncListener gestisce automaticamente la sincronizzazione quando viene montato in root.tsx.
Tipi di Messaggi Broadcast β
Il sistema usa i seguenti tipi di messaggi per sincronizzare le tab:
login: Quando un utente fa login con successologout: Quando un utente fa logoutlogin_pending_confirmation: Quando il login richiede conferma (sessione esistente)company_selected: Quando un utente seleziona un'aziendaback_to_companies: Quando un utente torna alla lista delle aziende
π― Best Practices β
Route Protection β
- Usa sempre
createProtectedLoader()per route che richiedono autenticazione - Usa
createGuestLoader()per login/register per reindirizzare utenti giΓ loggati - Non usare
clientLoaderper pagine pubbliche accessibili a tutti - Usa
AuthenticatedLayoutper pagine protette - Include automaticamente il menu di navigazione e il pulsante di logout
Data Fetching con TanStack Query β
β USA TanStack Query hooks per chiamate API
typescript// β RACCOMANDATO - TanStack Query import { useCompaniesList } from "@/hooks/api/auth/useAuthApi"; const { data, isLoading, error } = useCompaniesList(); // Nessun useEffect, nessun useRef necessario!β NON usare
useRefper prevenire doppi fetch - TanStack Query gestisce automaticamente la de-duplicazionetypescript// β VECCHIO - Non piΓΉ necessario const hasFetchedRef = useRef(false); useEffect(() => { if (!hasFetchedRef.current) { hasFetchedRef.current = true; execute(); } }, [execute]); // β NUOVO - TanStack Query gestisce tutto const { data } = useCompaniesList();β
useRefè OK per:- BroadcastChannel (non-fetch)
- Riferimenti DOM
- Valori che non devono causare re-render
- β MAI per prevenire fetch in StrictMode con TanStack Query
Import diretti (NO barrel files):
typescript// β Corretto import { useLogin } from "@/hooks/api/auth/useAuthApi"; // β Sbagliato import { useLogin } from "@/hooks/api/auth"; // No index.ts
Vantaggi TanStack Query β
- β Cache automatica (5 minuti default)
- β StrictMode safe - De-duplicazione automatica in development
- β Zero boilerplate - Nessun useEffect/useRef necessario
- β Invalidazioni coordinate - Mutations aggiornano automaticamente le query correlate
π Documentazione completa: docs/api/tanstack-query-guide.md
π¦ Struttura dei File β
app/
βββ routes.ts # Configurazione route
βββ lib/
β βββ api.ts # Utility API (apiRequest, ApiError)
β βββ query-client.ts # QueryClient TanStack Query
β βββ auth/
β βββ auth.ts # Servizio di autenticazione
β βββ loaders.ts # Factory functions (createProtectedLoader, createGuestLoader)
βββ store/
β βββ auth/
β βββ auth.store.ts # Zustand store con persist
βββ api/
β βββ auth/
β βββ auth.api.ts # Funzioni API autenticazione
βββ schemas/
β βββ api.schema.ts # Schema base API
β βββ auth/
β βββ auth.schema.ts # Schema autenticazione
βββ hooks/
β βββ api/
β β βββ auth/
β β βββ useAuthApi.ts # TanStack Query hooks per autenticazione
β βββ sync/
β βββ useBroadcastChannel.ts # Hook per sincronizzazione multi-tab
β βββ useAuthSyncListener.ts # Sync autenticazione tra tab
βββ components/
β βββ layout/
β βββ authenticated-layout.tsx # Layout per pagine protette
βββ routes/
βββ auth/
β βββ login.tsx # Route con createGuestLoader()
β βββ companies.tsx # Route con createProtectedLoader()
βββ welcome.tsx # Route con createProtectedLoader({ loadSettings: true })
βββ r.$path.tsx # Route moduli con iframeπ API di Autenticazione β
Il sistema fornisce accesso sia allo store Zustand che al servizio di autenticazione:
Uso dello Store Zustand (nei componenti React) β
import { useAuthStore } from "@/store/auth/auth.store";
import { useNavigate } from "react-router";
function MyComponent() {
const navigate = useNavigate();
// Accesso ottimizzato con selector (re-render solo quando cambia isAuthenticated)
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const userSettings = useAuthStore((state) => state.userSettings);
// Accesso alle actions
const logout = useAuthStore((state) => state.logout);
const handleLogout = () => {
// Pulisce lo store
logout();
// Naviga al login
navigate("/");
// Nota: AuthSyncListener gestisce il broadcast alle altre tab
};
return <div>{isAuthenticated ? "Logged in" : "Logged out"}</div>;
}Uso del Auth Service (fuori dai componenti React) β
import { authService } from "@/lib/auth/auth";
// Verifica se l'utente Γ¨ autenticato (legge da Zustand store)
if (authService.isAuthenticated()) {
// Logica per utenti autenticati
}
// Login (salva nello store Zustand + broadcast)
const response = await authService.login(username, password);
if (response) {
const path = authService.getRedirectPath(response.data.redirect, response.data.module);
navigate(path);
}
// Logout completo (pulisce store + broadcast + reindirizza)
authService.logout();Per maggiori dettagli sulle API HTTP disponibili, consulta api-auth.md. }
## π Modulo di Default
Il sistema supporta il redirect automatico al modulo di default dopo il login o la selezione di un'azienda.
### Come Funziona
Quando l'API restituisce `redirect: "main"` (dopo login o selezione azienda), il sistema:
1. **Controlla i settings utente** per il campo `default_module`
2. **Se esiste un modulo di default**:
- Reindirizza a `/r/{default_module}` invece che a `/welcome`
- Il modulo viene caricato in iframe
3. **Se non esiste un modulo di default** o Γ¨ vuoto:
- Reindirizza a `/welcome` (comportamento standard)
### Implementazione
La logica Γ¨ centralizzata nella funzione `getRedirectPath()` in `app/lib/auth/auth.ts`:
```typescript
// Esempio di risposta API con redirect "main"
{
"success": true,
"data": {
"redirect": "main", // Indica che si deve andare alla home
"module": null
}
}
// Il sistema legge dai settings utente:
{
"data": {
"default_module": "planning", // Modulo di default
// ... altri campi
}
}
// Risultato: redirect a /r/planning invece che a /welcomeCasi d'Uso β
Login con modulo di default:
- Utente fa login
- API ritorna
redirect: "main" - Sistema carica settings e trova
default_module: "planning" - Redirect a
/r/planninginvece che/welcome
Selezione azienda con modulo di default:
- Utente seleziona un'azienda
- Sistema carica i settings della nuova azienda
- Trova
default_module: "admin/dashboard" - Redirect a
/r/admin%2Fdashboardinvece che/welcome
Login senza modulo di default:
- Utente fa login
- Settings hanno
default_module: ""o assente - Redirect a
/welcome(comportamento standard)
Sincronizzazione Multi-Tab β
Il modulo di default viene rispettato anche nella sincronizzazione tra tab:
- Selezione azienda su Tab A: Tutte le tab caricano i nuovi settings e navigano al modulo di default (o
/welcome) - I settings vengono caricati in tutte le tab prima del redirect per garantire coerenza
βοΈ Come Funziona β
- L'utente naviga verso una route protetta
clientLoaderviene eseguito PRIMA del renderauthService.requireAuth()controlla l'autenticazione- Se autenticato β carica la route
- Se non autenticato β redirect a
/(login)
Il componente della route viene renderizzato solo se autorizzato.