Le flash d'UI "non-connecté → connecté" quand on utilise useVisibleTask$ pour vérifier l'auth.
AVANT (problématique):
┌──────────────────────────────────────────────────────────────┐
│ 1. SSR render → loading:true, isAuthenticated:false │
│ 2. HTML envoyé au navigateur (page non-connecté ou spinner) │
│ 3. Hydration client │
│ 4. useVisibleTask$ → fetch /api/auth/me avec cookie │
│ 5. Réponse → isAuthenticated:true → FLASH vers connecté │
└──────────────────────────────────────────────────────────────┘
APRÈS (corrigé):
┌──────────────────────────────────────────────────────────────┐
│ 1. plugin@auth.ts (onRequest) → lit cookie → appel API │
│ 2. SSR render avec isAuthenticated déjà correct │
│ 3. HTML envoyé (page correcte dès le départ) │
│ 4. Hydration client (pas de flash) │
└──────────────────────────────────────────────────────────────┘
src/routes/
├── plugin@auth.ts # S'exécute en PREMIER pour toutes les routes
├── layout.tsx # routeLoader$ pour exposer sharedMap aux composants
└── index.tsx # Utilise useContext(AuthContext)
import type { RequestHandler } from '@builder.io/qwik-city';
const API_URL = process.env.PUBLIC_API_URL || 'https://api.example.com';
export interface UserSession {
id: string;
email: string;
}
export const onRequest: RequestHandler = async (requestEvent) => {
const { cookie, sharedMap } = requestEvent;
// Récupérer le cookie httpOnly
const accessToken = cookie.get('access_token');
if (!accessToken?.value) {
sharedMap.set('user', null);
sharedMap.set('isAuthenticated', false);
return;
}
// Appeler l'API côté serveur avec le token
try {
const res = await fetch(`${API_URL}/api/auth/me`, {
headers: {
'Authorization': `Bearer ${accessToken.value}`,
'Content-Type': 'application/json'
}
});
const data = await res.json();
if (data.success && data.user) {
sharedMap.set('user', data.user);
sharedMap.set('isAuthenticated', true);
} else {
sharedMap.set('user', null);
sharedMap.set('isAuthenticated', false);
}
} catch (e) {
sharedMap.set('user', null);
sharedMap.set('isAuthenticated', false);
}
};
import { component$, Slot, useContextProvider, createContextId, useSignal } from '@builder.io/qwik';
import { routeLoader$, useNavigate, Link } from '@builder.io/qwik-city';
import type { UserSession } from './plugin@auth';
export interface AuthState {
user: UserSession | null;
isAuthenticated: boolean;
}
export const AuthContext = createContextId<AuthState>('auth-context');
// routeLoader$ pour récupérer l'auth depuis sharedMap
export const useAuthLoader = routeLoader$<AuthState>(({ sharedMap }) => {
const user = sharedMap.get('user') as UserSession | null;
const isAuthenticated = sharedMap.get('isAuthenticated') as boolean;
return {
user: user || null,
isAuthenticated: isAuthenticated || false
};
});
export default component$(() => {
const authData = useAuthLoader();
// Signal pour gérer logout dynamique dans la session
const loggedOut = useSignal(false);
const authState: AuthState = {
user: loggedOut.value ? null : authData.value.user,
isAuthenticated: loggedOut.value ? false : authData.value.isAuthenticated
};
useContextProvider(AuthContext, authState);
// ... reste du composant
});
import { useContext } from '@builder.io/qwik';
import { AuthContext } from './layout';
export default component$(() => {
const auth = useContext(AuthContext);
// PAS de loading state - l'auth est déjà disponible
if (!auth.isAuthenticated) {
return <LoginPrompt />;
}
return <Dashboard user={auth.user} />;
});
// Dans le layout d'une section protégée (ex: /admin/layout.tsx)
export const onRequest: RequestHandler = async ({ sharedMap, redirect }) => {
const isAuthenticated = sharedMap.get('isAuthenticated');
if (!isAuthenticated) {
throw redirect(302, '/auth/login');
}
};
import { server$ } from '@builder.io/qwik-city';
const getData = server$(async function() {
const user = this.sharedMap.get('user');
const isAuthenticated = this.sharedMap.get('isAuthenticated');
if (!isAuthenticated) {
throw new Error('Unauthorized');
}
// ... logique avec user
});
/gouroubleu/qwikpress)