diff --git a/app/(main)/dashboard/page.tsx b/app/(main)/dashboard/page.tsx index 74358f6..e45b009 100644 --- a/app/(main)/dashboard/page.tsx +++ b/app/(main)/dashboard/page.tsx @@ -36,61 +36,28 @@ const Dashboard = () => { const searchParams = useSearchParams(); const [selectedChantier, setSelectedChantier] = useState(null); const [showQuickView, setShowQuickView] = useState(false); - const [authProcessed, setAuthProcessed] = useState(false); - const [authError, setAuthError] = useState(null); - const [authInProgress, setAuthInProgress] = useState(false); - const [isHydrated, setIsHydrated] = useState(false); - // Flag global pour éviter les appels multiples (React 18 StrictMode) - const authProcessingRef = useRef(false); - // Mémoriser le code traité pour éviter les retraitements - const processedCodeRef = useRef(null); - // Flag pour éviter les redirections multiples - const redirectingRef = useRef(false); - - const currentCode = searchParams.get('code'); - const currentState = searchParams.get('state'); - - console.log('🏗️ Dashboard: SearchParams:', { - code: currentCode?.substring(0, 20) + '...', - state: currentState, - authProcessed, - processedCode: processedCodeRef.current?.substring(0, 20) + '...', - authInProgress: authInProgress - }); - - // Gérer l'hydratation pour éviter les erreurs SSR/CSR + // Nettoyer les paramètres OAuth de l'URL après le retour de Keycloak useEffect(() => { - setIsHydrated(true); - }, []); + if (typeof window === 'undefined') return; - // Réinitialiser authProcessed si on a un nouveau code d'autorisation - useEffect(() => { - if (!isHydrated) return; // Attendre l'hydratation - - if (currentCode && authProcessed && !authInProgress && processedCodeRef.current !== currentCode) { - console.log('🔄 Dashboard: Nouveau code détecté, réinitialisation authProcessed'); - setAuthProcessed(false); - processedCodeRef.current = null; - } - }, [currentCode, authProcessed, authInProgress, isHydrated]); - - // Fonction pour nettoyer l'URL des paramètres d'authentification - const cleanAuthParams = useCallback(() => { - if (typeof window === 'undefined') return; // Protection SSR - const url = new URL(window.location.href); - if (url.searchParams.has('code') || url.searchParams.has('state')) { + + // Nettoyer query string + if (url.searchParams.has('code') || url.searchParams.has('state') || url.searchParams.has('session_state')) { + console.log('🧹 Dashboard: Nettoyage des paramètres OAuth de l\'URL (query)'); url.search = ''; window.history.replaceState({}, '', url.toString()); -// Nettoyer sessionStorage après succès if (typeof window !== 'undefined') { sessionStorage.removeItem('oauth_code_processed'); } + } + + // Nettoyer fragment (hash) si Keycloak utilise implicit flow par erreur + if (url.hash && (url.hash.includes('code=') || url.hash.includes('state='))) { + console.log('🧹 Dashboard: Nettoyage des paramètres OAuth de l\'URL (fragment)'); + url.hash = ''; + window.history.replaceState({}, '', url.toString()); } }, []); - // Hooks pour les données et actions du dashboard - // Charger les données après authentification ou si pas de code d'autorisation - const shouldLoadData = authProcessed || !currentCode; - const { metrics, chantiersActifs, @@ -104,6 +71,8 @@ const Dashboard = () => { onRefresh: refresh }); + console.log('🏗️ Dashboard: État du chargement -', { loading, metricsLoaded: !!metrics, chantiersCount: chantiersActifs?.length || 0 }); + // Optimisations avec useMemo pour les calculs coûteux const formattedMetrics = useMemo(() => { if (!metrics) return null; @@ -194,129 +163,6 @@ const Dashboard = () => { chantierActions.handleQuickView(chantier); }, [chantierActions]); - // Traiter l'authentification Keycloak si nécessaire - useEffect(() => { - // Attendre l'hydratation avant de traiter l'authentification - if (!isHydrated) { - return; - } - - // Mode développement : ignorer l'authentification Keycloak - if (process.env.NEXT_PUBLIC_DEV_MODE === 'true' || process.env.NEXT_PUBLIC_SKIP_AUTH === 'true') { - console.log('🔧 Dashboard: Mode développement détecté, authentification ignorée'); - setAuthProcessed(true); - return; - } - - // Si l'authentification est déjà terminée, ne rien faire - if (authProcessed) { - return; - } - - // Si on est en train de rediriger, ne rien faire - if (redirectingRef.current) { - return; - } - - const processAuth = async () => { - try { - // Protection absolue contre les boucles - if (authInProgress || authProcessingRef.current) { - console.log('🛑 Dashboard: Processus d\'authentification déjà en cours, arrêt'); - return; - } - - // Les tokens sont maintenant stockés dans des cookies HttpOnly - // Le middleware les vérifiera automatiquement - // Pas besoin de vérifier localStorage - - const code = currentCode; - const state = currentState; - const error = searchParams.get('error'); - - if (error) { - setAuthError(`Erreur d'authentification: ${error}`); - setAuthProcessed(true); - return; - } - - // Vérifier si ce code a déjà été traité - if (code && !authProcessed && !authInProgress && !authProcessingRef.current && processedCodeRef.current !== code) { - try { - console.log('🔐 Traitement du code d\'autorisation Keycloak...', { code: code.substring(0, 20) + '...', state }); - - // Marquer IMMÉDIATEMENT dans sessionStorage pour empêcher double traitement - if (typeof window !== 'undefined') { - sessionStorage.setItem('oauth_code_processed', code); - } - - // Marquer l'authentification comme en cours pour éviter les appels multiples - authProcessingRef.current = true; - processedCodeRef.current = code; - setAuthInProgress(true); - - console.log('📡 Appel de /api/auth/token...'); - - // Utiliser fetch au lieu d'un formulaire pour éviter la boucle de redirection - const response = await fetch('/api/auth/token', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code, state: state || '' }) - }); - - if (response.ok) { - console.log('✅ Authentification réussie, tokens stockés dans les cookies'); - - // Nettoyer l'URL en enlevant les paramètres OAuth - window.history.replaceState({}, '', '/dashboard'); -// Nettoyer sessionStorage après succès if (typeof window !== 'undefined') { sessionStorage.removeItem('oauth_code_processed'); } - - // Marquer l'authentification comme terminée - setAuthProcessed(true); - setAuthInProgress(false); - authProcessingRef.current = false; - } else { - console.error("❌ Erreur lors de l'authentification"); - const errorData = await response.json(); - setAuthError(`Erreur lors de l'authentification: ${errorData.error || 'Erreur inconnue'}`); - setAuthProcessed(true); - setAuthInProgress(false); - authProcessingRef.current = false; - } - - } catch (error) { - console.error('❌ Erreur lors du traitement de l\'authentification:', error); - - // ARRÊTER LA BOUCLE : Ne pas rediriger automatiquement, juste marquer comme traité - console.log('🛑 Dashboard: Erreur d\'authentification, arrêt du processus pour éviter la boucle'); - - const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue lors de l\'authentification'; - setAuthError(`Erreur lors de l'authentification: ${errorMessage}`); - setAuthProcessed(true); - setAuthInProgress(false); - authProcessingRef.current = false; - } - } else { - setAuthProcessed(true); - setAuthInProgress(false); - authProcessingRef.current = false; - } - } catch (error) { - console.error('❌ Erreur générale lors du traitement de l\'authentification:', error); - const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue lors de l\'authentification'; - setAuthError(`Erreur lors de l'authentification: ${errorMessage}`); - setAuthProcessed(true); - setAuthInProgress(false); - authProcessingRef.current = false; - } - }; - - processAuth(); - }, [currentCode, currentState, authProcessed, authInProgress, isHydrated]); - - - - const actionBodyTemplate = useCallback((rowData: ChantierActif) => { const actions: ActionButtonType[] = ['VIEW', 'PHASES', 'PLANNING', 'STATS', 'MENU']; @@ -385,68 +231,6 @@ const Dashboard = () => { ); }, [handleQuickView, router, chantierActions]); - - - // Attendre l'hydratation pour éviter les erreurs SSR/CSR - if (!isHydrated) { - return ( -
-
-
-
- -
Chargement...
-

Initialisation de l'application

-
-
-
-
- ); - } - - // Afficher le chargement pendant le traitement de l'authentification - if (!authProcessed) { - return ( -
-
-
-
- -
Authentification en cours...
-

Traitement des informations de connexion

-
-
-
-
- ); - } - - - - - // Afficher l'erreur d'authentification - if (authError) { - return ( -
-
-
-
- -
Erreur d'authentification
-

{authError}

-
-
-
-
- ); - } - if (loading) { return (
diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx index 03e023a..82a2e67 100644 --- a/app/(main)/page.tsx +++ b/app/(main)/page.tsx @@ -12,6 +12,7 @@ import { Button } from 'primereact/button'; import { LayoutContext } from '../../layout/context/layoutcontext'; import { PrimeReactContext } from 'primereact/api'; import type { ColorScheme, Page } from '@/types'; +import { redirectToLogin } from '@/lib/auth'; const LandingPage: Page = () => { const { layoutConfig, setLayoutConfig } = useContext(LayoutContext); @@ -140,13 +141,13 @@ const LandingPage: Page = () => {
  • - + redirectToLogin()} className="p-ripple flex m-0 md:ml-5 md:px-0 px-3 py-3 text-gray-800 font-medium line-height-3 hover:text-gray-800 cursor-pointer"> Connexion
  • - + redirectToLogin()} className="p-ripple flex m-0 md:ml-5 md:px-0 px-3 py-3 text-gray-800 font-medium line-height-3 hover:text-gray-800 cursor-pointer"> Inscription @@ -182,7 +183,7 @@ const LandingPage: Page = () => { Bienvenue sur BTP Xpress

    Votre plateforme de gestion BTP moderne

    - + redirectToLogin()} className="p-button text-gray-700 bg-cyan-500 border-cyan-500 font-bold border-round" style={{ mixBlendMode: 'multiply', padding: ' 0.858rem 1.142rem' }}> Commencer
  • diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts deleted file mode 100644 index bf1020b..0000000 --- a/app/api/auth/login/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -const KEYCLOAK_URL = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://security.lions.dev'; -const KEYCLOAK_REALM = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'btpxpress'; -const CLIENT_ID = process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || 'btpxpress-frontend'; -const REDIRECT_URI = process.env.NEXT_PUBLIC_APP_URL - ? `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback` - : 'https://btpxpress.lions.dev/auth/callback'; - -export async function GET(request: NextRequest) { - console.log('🔐 Login API called'); - - // Générer un state aléatoire pour CSRF protection - const state = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); - - // Construire l'URL d'autorisation Keycloak - const authUrl = new URL(`${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/auth`); - - authUrl.searchParams.set('client_id', CLIENT_ID); - authUrl.searchParams.set('redirect_uri', REDIRECT_URI); - authUrl.searchParams.set('response_type', 'code'); - authUrl.searchParams.set('scope', 'openid profile email'); - authUrl.searchParams.set('state', state); - - console.log('✅ Redirecting to Keycloak:', authUrl.toString()); - - // Rediriger vers Keycloak pour l'authentification - return NextResponse.redirect(authUrl.toString()); -} diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts deleted file mode 100644 index ea480e5..0000000 --- a/app/api/auth/logout/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { cookies } from 'next/headers'; - -const KEYCLOAK_URL = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://security.lions.dev'; -const KEYCLOAK_REALM = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'btpxpress'; -const CLIENT_ID = process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || 'btpxpress-frontend'; -const POST_LOGOUT_REDIRECT_URI = process.env.NEXT_PUBLIC_APP_URL || 'https://btpxpress.lions.dev'; - -export async function GET(request: NextRequest) { - console.log('🚪 Logout API called'); - - const cookieStore = await cookies(); - - // Récupérer l'id_token avant de supprimer les cookies - const idToken = cookieStore.get('id_token')?.value; - - // Supprimer tous les cookies d'authentification - cookieStore.delete('access_token'); - cookieStore.delete('refresh_token'); - cookieStore.delete('id_token'); - cookieStore.delete('token_expires_at'); - - console.log('✅ Authentication cookies deleted'); - - // Si on a un id_token, on fait un logout Keycloak - if (idToken) { - const logoutUrl = new URL( - `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/logout` - ); - - logoutUrl.searchParams.set('client_id', CLIENT_ID); - logoutUrl.searchParams.set('post_logout_redirect_uri', POST_LOGOUT_REDIRECT_URI); - logoutUrl.searchParams.set('id_token_hint', idToken); - - console.log('✅ Redirecting to Keycloak logout'); - - return NextResponse.redirect(logoutUrl.toString()); - } - - // Sinon, rediriger directement vers la page d'accueil - console.log('✅ Redirecting to home page'); - return NextResponse.redirect(POST_LOGOUT_REDIRECT_URI); -} - -export async function POST(request: NextRequest) { - // Même logique pour POST - return GET(request); -} diff --git a/app/api/auth/token/route.ts b/app/api/auth/token/route.ts deleted file mode 100644 index e6a8c40..0000000 --- a/app/api/auth/token/route.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { cookies } from 'next/headers'; - -const KEYCLOAK_URL = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://security.lions.dev'; -const KEYCLOAK_REALM = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'btpxpress'; -const CLIENT_ID = process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || 'btpxpress-frontend'; -const REDIRECT_URI = process.env.NEXT_PUBLIC_APP_URL - ? `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback` - : 'https://btpxpress.lions.dev/auth/callback'; - -const TOKEN_ENDPOINT = `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`; - -export async function POST(request: NextRequest) { - console.log('🔐 Token exchange API called'); - - try { - const body = await request.json(); - const { code, state } = body; - - if (!code) { - console.error('❌ No authorization code provided'); - return NextResponse.json( - { error: 'Code d\'autorisation manquant' }, - { status: 400 } - ); - } - - console.log('✅ Exchanging code with Keycloak...'); - console.log('📍 Token endpoint:', TOKEN_ENDPOINT); - console.log('📍 Client ID:', CLIENT_ID); - console.log('📍 Redirect URI:', REDIRECT_URI); - - // Préparer les paramètres pour l'échange de code - const params = new URLSearchParams({ - grant_type: 'authorization_code', - client_id: CLIENT_ID, - code: code, - redirect_uri: REDIRECT_URI, - }); - - // Échanger le code contre des tokens - const tokenResponse = await fetch(TOKEN_ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: params.toString(), - }); - - if (!tokenResponse.ok) { - const errorText = await tokenResponse.text(); - console.error('❌ Keycloak token exchange failed:', tokenResponse.status, errorText); - - let errorData; - try { - errorData = JSON.parse(errorText); - } catch { - errorData = { error: 'Token exchange failed', details: errorText }; - } - - return NextResponse.json( - { - error: errorData.error || 'Échec de l\'échange de token', - error_description: errorData.error_description || 'Erreur lors de la communication avec Keycloak', - details: errorData - }, - { status: tokenResponse.status } - ); - } - - const tokens = await tokenResponse.json(); - console.log('✅ Tokens received from Keycloak'); - - // Stocker les tokens dans des cookies HttpOnly sécurisés - const cookieStore = await cookies(); - const isProduction = process.env.NODE_ENV === 'production'; - - // Access token (durée: expires_in secondes) - cookieStore.set('access_token', tokens.access_token, { - httpOnly: true, - secure: isProduction, - sameSite: 'lax', - maxAge: tokens.expires_in || 300, // Par défaut 5 minutes - path: '/', - }); - - // Refresh token (durée plus longue) - if (tokens.refresh_token) { - cookieStore.set('refresh_token', tokens.refresh_token, { - httpOnly: true, - secure: isProduction, - sameSite: 'lax', - maxAge: tokens.refresh_expires_in || 1800, // Par défaut 30 minutes - path: '/', - }); - } - - // ID token - if (tokens.id_token) { - cookieStore.set('id_token', tokens.id_token, { - httpOnly: true, - secure: isProduction, - sameSite: 'lax', - maxAge: tokens.expires_in || 300, - path: '/', - }); - } - - // Stocker aussi le temps d'expiration - cookieStore.set('token_expires_at', String(Date.now() + (tokens.expires_in * 1000)), { - httpOnly: true, - secure: isProduction, - sameSite: 'lax', - maxAge: tokens.expires_in || 300, - path: '/', - }); - - console.log('✅ Tokens stored in HttpOnly cookies'); - - // Retourner une réponse de succès (sans les tokens) - return NextResponse.json({ - success: true, - message: 'Authentification réussie', - expires_in: tokens.expires_in, - }); - - } catch (error) { - console.error('❌ Error in token exchange API:', error); - - return NextResponse.json( - { - error: 'Erreur serveur', - message: error instanceof Error ? error.message : 'Erreur inconnue' - }, - { status: 500 } - ); - } -} diff --git a/app/api/auth/userinfo/route.ts b/app/api/auth/userinfo/route.ts deleted file mode 100644 index f096ebb..0000000 --- a/app/api/auth/userinfo/route.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { cookies } from 'next/headers'; - -const KEYCLOAK_URL = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://security.lions.dev'; -const KEYCLOAK_REALM = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'btpxpress'; - -const USERINFO_ENDPOINT = `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/userinfo`; - -export async function GET(request: NextRequest) { - console.log('👤 Userinfo API called'); - - try { - const cookieStore = await cookies(); - const accessToken = cookieStore.get('access_token')?.value; - - if (!accessToken) { - console.error('❌ No access token found'); - return NextResponse.json( - { error: 'Non authentifié', authenticated: false }, - { status: 401 } - ); - } - - console.log('✅ Access token found, fetching user info from Keycloak'); - - // Récupérer les informations utilisateur depuis Keycloak - const userinfoResponse = await fetch(USERINFO_ENDPOINT, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - }, - }); - - if (!userinfoResponse.ok) { - const errorText = await userinfoResponse.text(); - console.error('❌ Keycloak userinfo failed:', userinfoResponse.status, errorText); - - // Si le token est invalide ou expiré - if (userinfoResponse.status === 401) { - // Supprimer les cookies invalides - cookieStore.delete('access_token'); - cookieStore.delete('refresh_token'); - cookieStore.delete('id_token'); - cookieStore.delete('token_expires_at'); - - return NextResponse.json( - { error: 'Token expiré ou invalide', authenticated: false }, - { status: 401 } - ); - } - - return NextResponse.json( - { - error: 'Erreur lors de la récupération des informations utilisateur', - authenticated: false - }, - { status: userinfoResponse.status } - ); - } - - const userinfo = await userinfoResponse.json(); - console.log('✅ User info retrieved:', userinfo.preferred_username || userinfo.sub); - - return NextResponse.json({ - authenticated: true, - user: userinfo, - }); - - } catch (error) { - console.error('❌ Error in userinfo API:', error); - - return NextResponse.json( - { - error: 'Erreur serveur', - authenticated: false, - message: error instanceof Error ? error.message : 'Erreur inconnue' - }, - { status: 500 } - ); - } -} diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx index 08f8461..ce313a4 100644 --- a/app/auth/login/page.tsx +++ b/app/auth/login/page.tsx @@ -4,6 +4,7 @@ import React, { Suspense } from 'react'; import { useSearchParams } from 'next/navigation'; import { Button } from 'primereact/button'; import { Card } from 'primereact/card'; +import { redirectToLogin } from '@/lib/auth'; function LoginContent() { const searchParams = useSearchParams(); @@ -15,8 +16,8 @@ function LoginContent() { sessionStorage.setItem('returnUrl', returnUrl); } - // Rediriger vers l'API de login qui initiera le flux OAuth - window.location.href = '/api/auth/login'; + // Rediriger vers Keycloak pour l'authentification + redirectToLogin(returnUrl); }; return ( diff --git a/app/page.tsx b/app/page.tsx index 2e00e4a..dfe99b8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -11,6 +11,7 @@ import { Button } from 'primereact/button'; import { LayoutContext } from '../layout/context/layoutcontext'; import { PrimeReactContext } from 'primereact/api'; import type { ColorScheme } from '@/types'; +import { redirectToLogin } from '@/lib/auth'; const LandingPage = () => { const router = useRouter(); @@ -110,7 +111,7 @@ const LandingPage = () => {
  • - window.location.href = 'http://localhost:8080/api/v1/auth/login'} className="p-ripple flex m-0 md:ml-5 md:px-0 px-3 py-3 text-gray-800 font-medium line-height-3 hover:text-gray-800 cursor-pointer"> + redirectToLogin()} className="p-ripple flex m-0 md:ml-5 md:px-0 px-3 py-3 text-gray-800 font-medium line-height-3 hover:text-gray-800 cursor-pointer"> Connexion @@ -168,7 +169,7 @@ const LandingPage = () => { Facturation automatisée - window.location.href = 'http://localhost:8080/api/v1/auth/login'} className="p-button text-white bg-orange-500 border-orange-500 font-bold border-round cursor-pointer mr-3 shadow-3" style={{ padding: '1.2rem 2.5rem', fontSize: '1.2rem' }}> + redirectToLogin()} className="p-button text-white bg-orange-500 border-orange-500 font-bold border-round cursor-pointer mr-3 shadow-3" style={{ padding: '1.2rem 2.5rem', fontSize: '1.2rem' }}> Démarrer maintenant @@ -469,7 +470,7 @@ const LandingPage = () => {
  • - - - - -
    -
    - - BTP Xpress -
    -

    - Construisez l'avenir -

    -

    avec BTP Xpress, la suite logicielle qui transforme le BTP

    -

    - Fini les chantiers qui dérapent, les budgets qui explosent et les équipes désorganisées. - BTP Xpress centralise tout : de la première estimation au dernier m² livré, - pilotez vos projets avec la précision d'un architecte et l'efficacité d'un chef de chantier expérimenté. -

    - -
    -
    - - Gestion complète de projets -
    -
    - - Suivi temps réel -
    -
    - - Facturation automatisée -
    -
    - window.location.href = '/api/auth/login'} className="p-button text-white bg-orange-500 border-orange-500 font-bold border-round cursor-pointer mr-3 shadow-3" style={{ padding: '1.2rem 2.5rem', fontSize: '1.2rem' }}> - - Démarrer maintenant - - - - Voir les fonctionnalités - -
    - -
    -
    -

    Spécialisé pour tous les métiers du BTP

    -

    Une solution adaptée à chaque corps de métier

    -
    -
    -
    -
    - - Gros Œuvre & Structure - Béton, maçonnerie, charpente : gérez vos approvisionnements, planifiez vos coulages, suivez vos cadences et optimisez vos rotations d'équipes. -
    -
    -
    -
    - - Second Œuvre & Finitions - Électricité, plomberie, peinture, carrelage : coordonnez vos interventions, gérez vos stocks et respectez les délais de livraison. -
    -
    -
    -
    - - Travaux Publics & VRD - Terrassement, voirie, réseaux : planifiez vos chantiers, gérez votre matériel et optimisez vos déplacements d'équipes. -
    -
    -
    -
    - - Maîtrise d'Œuvre & AMO - Pilotage de projets, coordination, suivi budgétaire : centralisez tous vos dossiers et collaborez efficacement avec tous les intervenants. -
    -
    -
    -
    - - -
    -
    -

    - BTP Xpress en action -

    -

    - Découvrez comment nos clients transforment leur activité BTP avec des outils pensés pour leur réussite -

    -
    -
    -
    - -
    -50%
    -
    Temps administratif
    -
    Automatisation des tâches répétitives
    -
    -
    -
    -
    - -
    +35%
    -
    Rentabilité
    -
    Optimisation des coûts et marges
    -
    -
    -
    -
    - -
    98%
    -
    Satisfaction client
    -
    Respect des délais et qualité
    -
    -
    -
    -
    -
    - -
    -
    -

    La différence BTP Xpress : des résultats concrets

    -
    -
    - - ); -}; - -export default LandingPage; diff --git a/components/ClientProviders.tsx b/components/ClientProviders.tsx index 89d99f2..3935059 100644 --- a/components/ClientProviders.tsx +++ b/components/ClientProviders.tsx @@ -2,6 +2,7 @@ import { LayoutProvider } from '../layout/context/layoutcontext'; import { PrimeReactProvider } from 'primereact/api'; +import { KeycloakProvider } from '../contexts/KeycloakContext'; import { AuthProvider } from '../contexts/AuthContext'; import { DevAuthProvider } from './auth/DevAuthProvider'; import { useServerStatusInit } from '../hooks/useServerStatusInit'; @@ -12,11 +13,13 @@ export function ClientProviders({ children }: { children: React.ReactNode }) { return ( - - - {children} - - + + + + {children} + + + ); } \ No newline at end of file diff --git a/components/ProtectedLayout.tsx b/components/ProtectedLayout.tsx index 1272f8c..1fee124 100644 --- a/components/ProtectedLayout.tsx +++ b/components/ProtectedLayout.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useAuth } from '../contexts/AuthContext'; +import { useKeycloak } from '../contexts/KeycloakContext'; import LoadingSpinner from './ui/LoadingSpinner'; import { useRouter, useSearchParams, usePathname } from 'next/navigation'; import { useEffect, useRef, useMemo } from 'react'; @@ -18,6 +19,7 @@ const ProtectedLayout: React.FC = ({ requiredPermissions = [] }) => { const { isAuthenticated, isLoading, user, hasRole, hasPermission } = useAuth(); + const { keycloak } = useKeycloak(); const router = useRouter(); const searchParams = useSearchParams(); const pathname = usePathname(); @@ -26,8 +28,8 @@ const ProtectedLayout: React.FC = ({ // Vérifier s'il y a un code d'autorisation dans l'URL // Utiliser useMemo pour éviter les re-rendus inutiles const hasAuthCode = useMemo(() => { - return searchParams.get('code') !== null && pathname === '/dashboard'; - }, [searchParams, pathname]); + return searchParams.get('code') !== null; + }, [searchParams]); useEffect(() => { console.log('🔍 ProtectedLayout useEffect:', { @@ -48,11 +50,17 @@ const ProtectedLayout: React.FC = ({ // Marquer comme redirigé pour éviter les boucles redirectedRef.current = true; - // Rediriger vers la page de connexion avec l'URL de retour - const searchParamsStr = searchParams.toString(); - const currentPath = pathname + (searchParamsStr ? `?${searchParamsStr}` : ''); - console.log('🔒 ProtectedLayout: Redirection vers /api/auth/login'); - window.location.href = `/api/auth/login?redirect=${encodeURIComponent(currentPath)}`; + // Rediriger vers Keycloak avec l'URL de retour + console.log('🔒 ProtectedLayout: Redirection vers Keycloak'); + if (keycloak) { + const searchParamsStr = searchParams.toString(); + const currentPath = pathname + (searchParamsStr ? `?${searchParamsStr}` : ''); + const redirectUri = currentPath && currentPath !== '/' + ? `${window.location.origin}${currentPath}` + : `${window.location.origin}/dashboard`; + + keycloak.login({ redirectUri }); + } } else if (hasAuthCode) { console.log('🔓 ProtectedLayout: Code d\'autorisation détecté, pas de redirection'); } else if (isAuthenticated) { @@ -60,7 +68,7 @@ const ProtectedLayout: React.FC = ({ } else if (isLoading) { console.log('⏳ ProtectedLayout: Chargement en cours, pas de redirection'); } - }, [isAuthenticated, isLoading, hasAuthCode, pathname]); + }, [isAuthenticated, isLoading, hasAuthCode, pathname, keycloak]); useEffect(() => { if (isAuthenticated && user) { diff --git a/components/layout/AppLayout.tsx b/components/layout/AppLayout.tsx index 658b508..c7cd4c1 100644 --- a/components/layout/AppLayout.tsx +++ b/components/layout/AppLayout.tsx @@ -12,6 +12,7 @@ import { StyleClass } from 'primereact/styleclass'; import { useRouter } from 'next/navigation'; import { useDevAuth } from '../auth/DevAuthProvider'; import Link from 'next/link'; +import { redirectToLogin } from '@/lib/auth'; interface AppLayoutProps { children: React.ReactNode; @@ -114,7 +115,7 @@ export const AppLayout: React.FC = ({ children }) => { icon: 'pi pi-sign-out', command: () => { logout(); - window.location.href = '/api/auth/login'; + redirectToLogin(); } } ]; diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx index 269dcbf..7d320ae 100644 --- a/contexts/AuthContext.tsx +++ b/contexts/AuthContext.tsx @@ -1,7 +1,8 @@ 'use client'; import React, { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react'; -import { initKeycloak, keycloakInitOptions, RoleUtils, BTP_ROLES, KEYCLOAK_TIMEOUTS, KEYCLOAK_REDIRECTS } from '@/config/keycloak'; +import { useKeycloak } from './KeycloakContext'; +import { RoleUtils, BTP_ROLES } from '@/config/keycloak'; import { useRouter } from 'next/navigation'; // Types pour l'authentification @@ -61,7 +62,10 @@ interface AuthProviderProps { // Provider d'authentification export const AuthProvider: React.FC = ({ children }) => { const router = useRouter(); - + + // Utiliser le contexte Keycloak + const { keycloak, authenticated, loading, token, login: keycloakLogin, logout: keycloakLogout, updateToken: keycloakUpdateToken } = useKeycloak(); + // État de l'authentification const [authState, setAuthState] = useState({ isAuthenticated: false, @@ -178,11 +182,10 @@ export const AuthProvider: React.FC = ({ children }) => { } }, [fetchUserInfo, extractUserInfo]); - // Fonction de connexion + // Fonction de connexion - utilise Keycloak JS SDK const login = useCallback(async (): Promise => { try { - // Redirection directe vers l'API d'authentification - window.location.href = '/api/auth/login'; + keycloakLogin(); } catch (error) { console.error('Erreur lors de la connexion:', error); setAuthState(prev => ({ @@ -191,21 +194,12 @@ export const AuthProvider: React.FC = ({ children }) => { isLoading: false, })); } - }, []); + }, [keycloakLogin]); - // Fonction de déconnexion + // Fonction de déconnexion - utilise Keycloak JS SDK const logout = useCallback(async (): Promise => { try { - // Nettoyer les tokens locaux - localStorage.removeItem('accessToken'); - localStorage.removeItem('refreshToken'); - localStorage.removeItem('idToken'); - - // Supprimer le cookie - document.cookie = 'keycloak-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; - - // Redirection directe vers l'API de déconnexion - window.location.href = '/api/auth/logout'; + keycloakLogout(); } catch (error) { console.error('Erreur lors de la déconnexion:', error); // Forcer la déconnexion locale même en cas d'erreur @@ -219,45 +213,32 @@ export const AuthProvider: React.FC = ({ children }) => { }); router.push('/'); } - }, [router]); + }, [keycloakLogout, router]); - // Fonction pour rafraîchir l'authentification + // Fonction pour rafraîchir l'authentification - utilise Keycloak SDK const refreshAuth = useCallback(async (): Promise => { try { - // Vérifier les tokens stockés - const accessToken = localStorage.getItem('accessToken'); - const refreshToken = localStorage.getItem('refreshToken'); - - if (!refreshToken) { - await logout(); - return; + if (keycloak && authenticated) { + await keycloakUpdateToken(); } - - // TODO: Implémenter le rafraîchissement via API si nécessaire - console.log('Rafraîchissement des tokens...'); - } catch (error) { console.error('Erreur lors du rafraîchissement du token:', error); await logout(); } - }, [logout]); + }, [keycloak, authenticated, keycloakUpdateToken, logout]); - // Fonction pour mettre à jour le token + // Fonction pour mettre à jour le token - utilise Keycloak SDK const updateToken = useCallback(async (minValidity: number = 30): Promise => { try { - // Vérifier si le token est encore valide - const accessToken = localStorage.getItem('accessToken'); - if (!accessToken) { - return false; + if (keycloak && authenticated) { + return await keycloakUpdateToken(); } - - // TODO: Vérifier l'expiration du token et rafraîchir si nécessaire - return true; + return false; } catch (error) { console.error('Erreur lors de la mise à jour du token:', error); return false; } - }, []); + }, [keycloak, authenticated, keycloakUpdateToken]); // Fonctions utilitaires pour les rôles et permissions const hasRole = useCallback((role: string): boolean => { @@ -277,65 +258,39 @@ export const AuthProvider: React.FC = ({ children }) => { return RoleUtils.isRoleHigher(authState.user.highestRole, role); }, [authState.user]); - // Initialisation de Keycloak - Désactivée pour utiliser l'authentification manuelle + // Synchroniser l'état avec KeycloakContext useEffect(() => { - const initializeKeycloak = async () => { - try { - // Vérifier s'il y a des tokens stockés localement - const accessToken = localStorage.getItem('accessToken'); - const refreshToken = localStorage.getItem('refreshToken'); - const idToken = localStorage.getItem('idToken'); + const syncAuthState = async () => { + if (loading) { + setAuthState({ + isAuthenticated: false, + isLoading: true, + user: null, + token: null, + refreshToken: null, + error: null, + }); + return; + } - if (accessToken) { - // Stocker aussi dans un cookie pour le middleware - document.cookie = `keycloak-token=${accessToken}; path=/; max-age=3600; SameSite=Lax`; + if (authenticated && keycloak && token) { + // Essayer de récupérer les informations utilisateur depuis l'API + let user = await fetchUserInfo(token); - // Récupérer les informations utilisateur depuis l'API - try { - const user = await fetchUserInfo(accessToken); - - if (user) { - setAuthState({ - isAuthenticated: true, - isLoading: false, - user, - token: accessToken, - refreshToken: refreshToken, - error: null, - }); - return; - } - } catch (error) { - console.warn('Impossible de récupérer les informations utilisateur depuis l\'API, utilisation des données par défaut'); - } - - // Fallback avec des données par défaut si l'API ne répond pas - setAuthState({ - isAuthenticated: true, - isLoading: false, - user: { - id: 'dev-user-001', - username: 'admin.btpxpress', - email: 'admin@btpxpress.com', - firstName: 'Jean-Michel', - lastName: 'Martineau', - fullName: 'Jean-Michel Martineau', - roles: [BTP_ROLES.SUPER_ADMIN, BTP_ROLES.ADMIN, BTP_ROLES.DIRECTEUR], - permissions: RoleUtils.getUserPermissions([BTP_ROLES.SUPER_ADMIN]), - highestRole: BTP_ROLES.SUPER_ADMIN, - isAdmin: true, - isManager: true, - isEmployee: false, - isClient: false, - }, - token: accessToken, - refreshToken: refreshToken, - error: null, - }); - return; + // Si l'API ne répond pas, extraire du token JWT + if (!user) { + user = extractUserInfo(keycloak); } - // Pas de tokens, rester non authentifié + setAuthState({ + isAuthenticated: true, + isLoading: false, + user, + token: token, + refreshToken: keycloak.refreshToken || null, + error: null, + }); + } else { setAuthState({ isAuthenticated: false, isLoading: false, @@ -344,33 +299,13 @@ export const AuthProvider: React.FC = ({ children }) => { refreshToken: null, error: null, }); - - } catch (error) { - console.error('Erreur lors de l\'initialisation de l\'authentification:', error); - setAuthState({ - isAuthenticated: false, - isLoading: false, - user: null, - token: null, - refreshToken: null, - error: 'Erreur lors de l\'initialisation de l\'authentification', - }); } }; - initializeKeycloak(); - }, [updateAuthState, refreshAuth, logout]); + syncAuthState(); + }, [authenticated, loading, token, keycloak, fetchUserInfo, extractUserInfo]); - // Rafraîchissement automatique du token - useEffect(() => { - if (!authState.isAuthenticated) return; - - const interval = setInterval(() => { - updateToken(); - }, KEYCLOAK_TIMEOUTS.SESSION_CHECK_INTERVAL * 1000); - - return () => clearInterval(interval); - }, [authState.isAuthenticated, updateToken]); + // Le rafraîchissement automatique du token est géré par KeycloakContext // Valeur du contexte const contextValue: AuthContextType = { diff --git a/contexts/KeycloakContext.tsx b/contexts/KeycloakContext.tsx new file mode 100644 index 0000000..9ee5638 --- /dev/null +++ b/contexts/KeycloakContext.tsx @@ -0,0 +1,119 @@ +'use client'; + +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import Keycloak from 'keycloak-js'; +import keycloak from '../lib/keycloak'; + +interface KeycloakContextType { + keycloak: Keycloak | null; + authenticated: boolean; + loading: boolean; + token: string | null; + login: () => void; + logout: () => void; + updateToken: () => Promise; +} + +const KeycloakContext = createContext(undefined); + +export const useKeycloak = () => { + const context = useContext(KeycloakContext); + if (!context) { + throw new Error('useKeycloak must be used within a KeycloakProvider'); + } + return context; +}; + +interface KeycloakProviderProps { + children: ReactNode; +} + +export const KeycloakProvider: React.FC = ({ children }) => { + const [authenticated, setAuthenticated] = useState(false); + const [loading, setLoading] = useState(true); + const [token, setToken] = useState(null); + + useEffect(() => { + const initKeycloak = async () => { + try { + console.log('🔐 Initializing Keycloak...'); + + const authenticated = await keycloak.init({ + onLoad: 'check-sso', + silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html', + pkceMethod: 'S256', + checkLoginIframe: false, // Désactivé pour éviter les problèmes CORS + flow: 'standard', // Force authorization_code flow (pas implicit/hybrid) + responseMode: 'query', // Force query string au lieu de fragment + }); + + console.log(`✅ Keycloak initialized. Authenticated: ${authenticated}`); + + setAuthenticated(authenticated); + setToken(keycloak.token || null); + + // Rafraîchir le token automatiquement + if (authenticated) { + setInterval(() => { + keycloak.updateToken(70).then((refreshed) => { + if (refreshed) { + console.log('🔄 Token refreshed'); + setToken(keycloak.token || null); + } + }).catch(() => { + console.error('❌ Failed to refresh token'); + setAuthenticated(false); + }); + }, 60000); // Toutes les 60 secondes + } + } catch (error) { + console.error('❌ Keycloak initialization failed:', error); + } finally { + setLoading(false); + } + }; + + initKeycloak(); + }, []); + + const login = () => { + keycloak.login({ + redirectUri: window.location.origin + '/dashboard', + }); + }; + + const logout = () => { + keycloak.logout({ + redirectUri: window.location.origin, + }); + }; + + const updateToken = async (): Promise => { + try { + const refreshed = await keycloak.updateToken(30); + if (refreshed) { + setToken(keycloak.token || null); + } + return refreshed; + } catch (error) { + console.error('Failed to update token', error); + return false; + } + }; + + const value: KeycloakContextType = { + keycloak, + authenticated, + loading, + token, + login, + logout, + updateToken, + }; + + return ( + + {children} + + ); +}; diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..ea2debc --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,78 @@ +/** + * Utilitaires d'authentification centralisés + * Utilise le SDK Keycloak JS pour toutes les opérations d'authentification + * + * IMPORTANT: Utilise un import conditionnel pour éviter les erreurs SSR + */ + +/** + * Récupère l'instance Keycloak de manière sécurisée (client-side uniquement) + */ +const getKeycloak = async () => { + if (typeof window === 'undefined') return null; + const { default: keycloak } = await import('./keycloak'); + return keycloak; +}; + +/** + * Redirige vers la page de connexion Keycloak + */ +export const redirectToLogin = async (returnUrl?: string) => { + if (typeof window === 'undefined') return; + + const keycloak = await getKeycloak(); + if (keycloak) { + const redirectUri = returnUrl + ? `${window.location.origin}${returnUrl}` + : `${window.location.origin}/dashboard`; + + keycloak.login({ redirectUri }); + } else { + console.error('❌ Keycloak non initialisé'); + } +}; + +/** + * Redirige vers la page de déconnexion Keycloak + */ +export const redirectToLogout = async () => { + if (typeof window === 'undefined') return; + + const keycloak = await getKeycloak(); + if (keycloak) { + keycloak.logout({ redirectUri: window.location.origin }); + } else { + console.error('❌ Keycloak non initialisé'); + } +}; + +/** + * Vérifie si l'utilisateur est authentifié + */ +export const isAuthenticated = async (): Promise => { + const keycloak = await getKeycloak(); + return keycloak?.authenticated ?? false; +}; + +/** + * Récupère le token d'accès actuel + */ +export const getAccessToken = async (): Promise => { + const keycloak = await getKeycloak(); + return keycloak?.token; +}; + +/** + * Rafraîchit le token si nécessaire + */ +export const refreshToken = async (minValidity: number = 30): Promise => { + const keycloak = await getKeycloak(); + if (!keycloak) return false; + + try { + return await keycloak.updateToken(minValidity); + } catch (error) { + console.error('❌ Erreur lors du rafraîchissement du token:', error); + return false; + } +}; diff --git a/lib/keycloak.ts b/lib/keycloak.ts new file mode 100644 index 0000000..21a74af --- /dev/null +++ b/lib/keycloak.ts @@ -0,0 +1,14 @@ +import Keycloak from 'keycloak-js'; + +// Configuration Keycloak +const keycloakConfig = { + url: process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://security.lions.dev', + realm: process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'btpxpress', + clientId: process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || 'btpxpress-frontend', +}; + +// Créer une instance Keycloak uniquement côté client +// Pour éviter les erreurs SSR (document is not defined) +const keycloak = typeof window !== 'undefined' ? new Keycloak(keycloakConfig) : null; + +export default keycloak as Keycloak; diff --git a/middleware.ts b/middleware.ts index caac750..1579837 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,97 +1,17 @@ /** - * Middleware Next.js pour l'authentification OAuth avec Keycloak + * Middleware Next.js simplifié pour Frontend-Centric auth * - * Ce middleware protège les routes privées en vérifiant la présence - * d'un access_token dans les cookies HttpOnly. + * Avec Keycloak JS SDK, l'authentification est gérée côté client par KeycloakContext. + * Ce middleware laisse passer toutes les requêtes - la protection des routes est + * gérée par ProtectedLayout et AuthContext côté client. */ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; -// Routes publiques accessibles sans authentification -const PUBLIC_ROUTES = [ - '/', - '/auth/login', - '/auth/callback', - '/api/auth/login', - '/api/auth/callback', - '/api/auth/token', - '/api/health', -]; - -// Routes API publiques (patterns) -const PUBLIC_API_PATTERNS = [ - /^\/api\/auth\/.*/, - /^\/api\/health/, -]; - -// Vérifie si une route est publique -function isPublicRoute(pathname: string): boolean { - // Vérifier les routes exactes - if (PUBLIC_ROUTES.includes(pathname)) { - return true; - } - - // Vérifier les patterns - return PUBLIC_API_PATTERNS.some(pattern => pattern.test(pathname)); -} - export function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - - console.log('🔒 Middleware - Checking:', pathname); - - // Laisser passer les routes publiques - if (isPublicRoute(pathname)) { - console.log('✅ Middleware - Public route, allowing'); - return NextResponse.next(); - } - - // Laisser passer les fichiers statiques - if ( - pathname.startsWith('/_next') || - pathname.startsWith('/static') || - pathname.includes('.') - ) { - return NextResponse.next(); - } - - // Vérifier la présence du token d'authentification - const accessToken = request.cookies.get('access_token'); - const tokenExpiresAt = request.cookies.get('token_expires_at'); - - if (!accessToken) { - console.log('❌ Middleware - No access token, redirecting to login'); - - // Rediriger vers la page de login avec l'URL de retour - const loginUrl = new URL('/auth/login', request.url); - loginUrl.searchParams.set('returnUrl', pathname); - - return NextResponse.redirect(loginUrl); - } - - // Vérifier si le token est expiré - if (tokenExpiresAt) { - const expiresAt = parseInt(tokenExpiresAt.value, 10); - const now = Date.now(); - - if (now >= expiresAt) { - console.log('❌ Middleware - Token expired, redirecting to login'); - - // Supprimer les cookies expirés - const response = NextResponse.redirect(new URL('/auth/login', request.url)); - response.cookies.delete('access_token'); - response.cookies.delete('refresh_token'); - response.cookies.delete('id_token'); - response.cookies.delete('token_expires_at'); - - return response; - } - } - - console.log('✅ Middleware - Authenticated, allowing'); - - // L'utilisateur est authentifié, laisser passer + // Laisser passer toutes les requêtes + // L'authentification est gérée côté client par Keycloak JS SDK return NextResponse.next(); } diff --git a/public/silent-check-sso.html b/public/silent-check-sso.html new file mode 100644 index 0000000..efe8698 --- /dev/null +++ b/public/silent-check-sso.html @@ -0,0 +1,11 @@ + + + + Silent SSO Check + + + + + diff --git a/services/ApiService.ts b/services/ApiService.ts index 20cfb91..ed5a49f 100644 --- a/services/ApiService.ts +++ b/services/ApiService.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { API_CONFIG } from '../config/api'; -import { keycloak, KEYCLOAK_TIMEOUTS } from '../config/keycloak'; +import keycloak from '../lib/keycloak'; class ApiService { private api = axios.create({ @@ -16,8 +16,8 @@ class ApiService { // Vérifier si Keycloak est initialisé et l'utilisateur authentifié if (keycloak && keycloak.authenticated) { try { - // Rafraîchir le token si nécessaire - await keycloak.updateToken(KEYCLOAK_TIMEOUTS.TOKEN_REFRESH_BEFORE_EXPIRY); + // Rafraîchir le token si nécessaire (70 secondes avant expiration) + await keycloak.updateToken(70); // Ajouter le token Bearer à l'en-tête Authorization if (keycloak.token) { @@ -29,22 +29,6 @@ class ApiService { keycloak.login(); throw error; } - } else { - // Fallback vers l'ancien système pour la rétrocompatibilité - let token = null; - try { - const authTokenItem = sessionStorage.getItem('auth_token') || localStorage.getItem('auth_token'); - if (authTokenItem) { - const parsed = JSON.parse(authTokenItem); - token = parsed.value; - } - } catch (e) { - token = localStorage.getItem('token'); - } - - if (token) { - config.headers['Authorization'] = `Bearer ${token}`; - } } return config; }, @@ -73,12 +57,10 @@ class ApiService { const hasAuthCode = currentUrl.includes('code=') && currentUrl.includes('/dashboard'); if (!hasAuthCode) { - // Fallback vers l'ancien système - localStorage.removeItem('token'); - localStorage.removeItem('user'); - localStorage.removeItem('auth_token'); - sessionStorage.removeItem('auth_token'); - window.location.href = '/api/auth/login'; + console.log('❌ Non authentifié, redirection vers Keycloak...'); + if (keycloak) { + keycloak.login(); + } } else { console.log('🔄 ApiService: Erreur 401 ignorée car authentification en cours...'); } diff --git a/services/api.ts b/services/api.ts index 2070ef2..4b1f0d8 100644 --- a/services/api.ts +++ b/services/api.ts @@ -4,7 +4,7 @@ import axios, { AxiosInstance, AxiosResponse } from 'axios'; import { API_CONFIG } from '../config/api'; -import { keycloak, KEYCLOAK_TIMEOUTS } from '../config/keycloak'; +import keycloak from '../lib/keycloak'; import { CacheService, CacheKeys } from './cacheService'; import { Client, @@ -42,18 +42,30 @@ class ApiService { // Interceptor pour les requêtes this.api.interceptors.request.use( async (config) => { - // Les tokens sont dans des cookies HttpOnly, automatiquement envoyés par le navigateur - // Pas besoin de les ajouter manuellement dans les headers - // Le header Authorization sera ajouté par le serveur en lisant les cookies - console.log('🔐 API Request:', config.url); + // Vérifier si Keycloak est initialisé et l'utilisateur authentifié + if (keycloak && keycloak.authenticated && keycloak.token) { + try { + // Rafraîchir le token si nécessaire (70 secondes avant expiration) + await keycloak.updateToken(70); + + // Ajouter le token Bearer à l'en-tête Authorization + config.headers['Authorization'] = `Bearer ${keycloak.token}`; + console.log('✅ Token ajouté à la requête'); + } catch (error) { + console.error('❌ Erreur lors de la mise à jour du token Keycloak:', error); + // En cas d'erreur, rediriger vers la page de connexion + keycloak.login(); + throw error; + } + } else { + console.warn('⚠️ Keycloak non authentifié, requête sans token'); + } + // Ajouter des en-têtes par défaut config.headers['X-Requested-With'] = 'XMLHttpRequest'; - // Assurer que les cookies sont envoyés avec les requêtes CORS - config.withCredentials = true; - return config; }, (error) => { @@ -103,16 +115,29 @@ class ApiService { const hasAuthCode = currentUrl.includes('code=') && currentUrl.includes('/dashboard'); if (!hasAuthCode) { - console.log('🔄 Token expiré ou absent, redirection vers la connexion...'); - // Sauvegarder la page actuelle pour y revenir après reconnexion - const currentPath = window.location.pathname + window.location.search; - sessionStorage.setItem('returnUrl', currentPath); + console.log('🔄 Token expiré ou absent, redirection vers Keycloak...'); - // Les cookies HttpOnly seront automatiquement nettoyés par l'expiration - // ou lors de la reconnexion. Pas besoin de manipulation côté client. - - // Rediriger vers la page de connexion - window.location.href = '/api/auth/login'; + // Essayer de rafraîchir le token Keycloak + if (keycloak && keycloak.authenticated) { + try { + await keycloak.updateToken(-1); // Force refresh + // Retry the original request + return this.api.request(error.config); + } catch (refreshError) { + console.error('❌ Impossible de rafraîchir le token, reconnexion requise'); + // Sauvegarder la page actuelle pour y revenir après reconnexion + const currentPath = window.location.pathname + window.location.search; + sessionStorage.setItem('returnUrl', currentPath); + // Rediriger vers Keycloak pour authentification + keycloak.login(); + } + } else { + // Pas authentifié, rediriger vers Keycloak + console.log('❌ Non authentifié, redirection vers Keycloak...'); + if (keycloak) { + keycloak.login(); + } + } } else { console.log('🔄 API Service: Erreur 401 ignorée car authentification en cours...'); }