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é.
-
-
- 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...');
}