Skip to content

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:

typescript
// 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: Se true, verifica sia l'autenticazione che la presenza dei settings utente. Se i settings non sono presenti, reindirizza a /welcome dove vengono caricati. Utile per route che dipendono dai dati di configurazione utente (azienda, moduli, preferenze).
    • options.loadSettings: Se true, 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:

typescript
export default [
    route("settings", "routes/settings.tsx"),
] satisfies RouteConfig;

Step 2: Aggiungi il clientLoader ​

Crea il file della route con protezione:

typescript
// 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: true verifica 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 /welcome usa loadSettings: true per caricare i settings PRIMA del render
  • Questo garantisce zero flash di contenuto e dati sempre disponibili

πŸ’‘ PerchΓ© serve clientLoader? Il clientLoader Γ¨ 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 /welcome PRIMA 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:

typescript
export default [
    route("register", "routes/register.tsx"),
] satisfies RouteConfig;

Step 2: Aggiungi il clientLoader ​

Crea il file con protezione guest-only:

typescript
// 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:

typescript
// 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 successo
  • logout: Quando un utente fa logout
  • login_pending_confirmation: Quando il login richiede conferma (sessione esistente)
  • company_selected: Quando un utente seleziona un'azienda
  • back_to_companies: Quando un utente torna alla lista delle aziende

🎯 Best Practices ​

Route Protection ​

  1. Usa sempre createProtectedLoader() per route che richiedono autenticazione
  2. Usa createGuestLoader() per login/register per reindirizzare utenti giΓ  loggati
  3. Non usare clientLoader per pagine pubbliche accessibili a tutti
  4. Usa AuthenticatedLayout per pagine protette - Include automaticamente il menu di navigazione e il pulsante di logout

Data Fetching con TanStack Query ​

  1. βœ… 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!
  2. ❌ NON usare useRef per prevenire doppi fetch - TanStack Query gestisce automaticamente la de-duplicazione

    typescript
    // ❌ 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();
  3. βœ… useRef Γ¨ OK per:

    • BroadcastChannel (non-fetch)
    • Riferimenti DOM
    • Valori che non devono causare re-render
    • ❌ MAI per prevenire fetch in StrictMode con TanStack Query
  4. 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) ​

typescript
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) ​

typescript
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 /welcome

Casi d'Uso ​

  1. Login con modulo di default:

    • Utente fa login
    • API ritorna redirect: "main"
    • Sistema carica settings e trova default_module: "planning"
    • Redirect a /r/planning invece che /welcome
  2. 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%2Fdashboard invece che /welcome
  3. 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 ​

  1. L'utente naviga verso una route protetta
  2. clientLoader viene eseguito PRIMA del render
  3. authService.requireAuth() controlla l'autenticazione
  4. Se autenticato β†’ carica la route
  5. Se non autenticato β†’ redirect a / (login)

Il componente della route viene renderizzato solo se autorizzato.

Documentazione Elerama Frontend