Authentification fonctionnelle via security.lions.dev

This commit is contained in:
DahoudG
2025-11-01 14:16:20 +00:00
parent a5adb84a62
commit 1d68878601
20 changed files with 387 additions and 1067 deletions

View File

@@ -36,61 +36,28 @@ const Dashboard = () => {
const searchParams = useSearchParams();
const [selectedChantier, setSelectedChantier] = useState<ChantierActif | null>(null);
const [showQuickView, setShowQuickView] = useState(false);
const [authProcessed, setAuthProcessed] = useState(false);
const [authError, setAuthError] = useState<string | null>(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<string | null>(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 (
<div className="grid">
<div className="col-12">
<div className="card">
<div className="text-center">
<ProgressSpinner style={{ width: '50px', height: '50px' }} />
<h5 className="mt-3">Chargement...</h5>
<p className="text-600">Initialisation de l'application</p>
</div>
</div>
</div>
</div>
);
}
// Afficher le chargement pendant le traitement de l'authentification
if (!authProcessed) {
return (
<div className="grid">
<div className="col-12">
<div className="card">
<div className="text-center">
<ProgressSpinner style={{ width: '50px', height: '50px' }} />
<h5 className="mt-3">Authentification en cours...</h5>
<p className="text-600">Traitement des informations de connexion</p>
</div>
</div>
</div>
</div>
);
}
// Afficher l'erreur d'authentification
if (authError) {
return (
<div className="grid">
<div className="col-12">
<div className="card">
<div className="text-center">
<i className="pi pi-exclamation-triangle text-red-500" style={{ fontSize: '4rem' }}></i>
<h5 className="text-red-500">Erreur d'authentification</h5>
<p className="text-600 mb-4">{authError}</p>
<Button
label="Retour à la connexion"
icon="pi pi-sign-in"
onClick={() => window.location.href = '/api/auth/login'}
className="p-button-outlined"
/>
</div>
</div>
</div>
</div>
);
}
if (loading) {
return (
<div className="grid">