From d89eaaa17f167699d5ab6818ea766155a5bc616b Mon Sep 17 00:00:00 2001 From: mathis Date: Wed, 30 Sep 2026 21:57:38 +0200 Subject: [PATCH] RELEASE V.4.1.0 : MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --> Refonte du système de login + Activation du compte (avec envoi d'un mail de confirmation). --- src/App.js | 16 +++ src/pages/activer-compte/ActiverCompte.js | 24 +++-- src/pages/login/Login.js | 118 ++++++++-------------- src/supabaseClient.js | 44 +------- 4 files changed, 76 insertions(+), 126 deletions(-) diff --git a/src/App.js b/src/App.js index 512df35..40e1957 100644 --- a/src/App.js +++ b/src/App.js @@ -6,6 +6,8 @@ import ActiverCompte from './pages/activer-compte/ActiverCompte'; import PanelRouter from './pages/router/PanelRouter'; import AppHeader from './components/header/AppHeader'; +import supabase from './supabaseClient'; + function App() { const [currentUser, setCurrentUser] = useState(() => { try { @@ -52,10 +54,24 @@ function App() { window.addEventListener('app:authChanged', onAuthChanged); window.addEventListener('unhandledrejection', onUnhandledRejection); + // Supabase Auth listener + const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => { + if (event === 'SIGNED_OUT') { + window.localStorage.removeItem('app_user'); + setCurrentUser(null); + if (window.location.pathname !== '/') { + window.location.href = '/'; + } + } + }); + // cleanup return () => { window.removeEventListener('app:authChanged', onAuthChanged); window.removeEventListener('unhandledrejection', onUnhandledRejection); + if (authListener && authListener.subscription) { + authListener.subscription.unsubscribe(); + } }; }, []); diff --git a/src/pages/activer-compte/ActiverCompte.js b/src/pages/activer-compte/ActiverCompte.js index a27adbb..c18406e 100644 --- a/src/pages/activer-compte/ActiverCompte.js +++ b/src/pages/activer-compte/ActiverCompte.js @@ -119,25 +119,31 @@ export default function ActiverCompte() { setLoading(true); try { - const { hash } = await import('bcryptjs'); - const passwordHash = await hash(password, 10); + // 1. Création du compte dans Supabase Auth (GoTrue) + const { data: authData, error: authError } = await supabase.auth.signUp({ + email: account.email, + password: password + }); + if (authError) { + setError(`Erreur lors de la création du compte Auth: ${authError.message}`); + setLoading(false); + return; + } + + // 2. Mise à jour du statut actif dans la table `utilisateurs` + // On ne sauvegarde plus le password_hash manuellement const { error: activationError } = await supabase .from('utilisateurs') .update({ - password_hash: passwordHash, + password_hash: null, // On vide l'ancien hash s'il y en avait un actif: true, updated_at: new Date().toISOString() }) .eq('id', account.id); if (activationError) { - await supabase - .from('utilisateurs') - .update({ actif: false, updated_at: new Date().toISOString() }) - .eq('id', account.id); - - setError('Activation impossible. L\'acces au compte a ete desactive.'); + setError("Le compte Auth a été créé mais l'activation du profil a échoué."); return; } diff --git a/src/pages/login/Login.js b/src/pages/login/Login.js index e809f0b..3d45d07 100644 --- a/src/pages/login/Login.js +++ b/src/pages/login/Login.js @@ -3,38 +3,6 @@ import supabase, { isSupabaseConfigured } from '../../supabaseClient'; import './Login.css'; -async function tryFallbackAuth(email, password) { - try { - const { data: userRow, error } = await supabase - .from('utilisateurs') - .select('id, email, nom, prenom, role, actif, password_hash, region_admin, region_id, region_nom, permissions') - .eq('email', email) - .single(); - - if (error || !userRow || userRow.actif === false || !userRow.password_hash) { - return null; - } - - const { compare } = await import('bcryptjs'); - const ok = await compare(password, userRow.password_hash); - if (!ok) return null; - - return { - id: userRow.id, - email: userRow.email, - nom: userRow.nom, - prenom: userRow.prenom, - name: [userRow.nom, userRow.prenom].filter(Boolean).join(' ').trim() || userRow.email, - role: userRow.role, - permissions: userRow.permissions || {}, - region_admin: userRow.region_admin === true, - region_id: userRow.region_id || null, - region_nom: userRow.region_nom || null - }; - } catch (_) { - return null; - } -} export default function Login({ onLogin }) { const emailRef = useRef(null); @@ -80,53 +48,55 @@ export default function Login({ onLogin }) { } try { - const { data, error: rpcError } = await supabase.rpc('authenticate_user', { - p_email: email, - p_password: password + const { data: authData, error: authError } = await supabase.auth.signInWithPassword({ + email, + password }); - let effectiveData = data; - if (rpcError || !((Array.isArray(data) && data.length) || (!Array.isArray(data) && data))) { - // Fallback de secours si la RPC est absente/mal configurée. - const fallbackUser = await tryFallbackAuth(email, password); - if (fallbackUser) { - effectiveData = [fallbackUser]; - } else if (rpcError) { - const msg = rpcError.message || rpcError.details || rpcError.code || JSON.stringify(rpcError); - console.error('RPC error', rpcError); - setError(`Erreur d'authentification: ${msg}`); - setLoading(false); - return; - } + if (authError || !authData.user) { + const msg = authError?.message || 'Identifiants invalides'; + setError(`Erreur d'authentification: ${msg}`); + setLoading(false); + return; } - const user = Array.isArray(effectiveData) ? (effectiveData.length ? effectiveData[0] : null) : effectiveData; - if (user && user.id) { - // S'assurer que permissions est au moins un objet vide pour que le RBAC applique les templates de base - user.permissions = user.permissions || {}; - - // Complète le profil avec les infos régionales nécessaires au RBAC front. - try { - const { data: profile } = await supabase - .from('utilisateurs') - .select('region_admin, region_id, region_nom, role, permissions') - .eq('id', user.id) - .single(); - if (profile) { - user.region_admin = profile.region_admin === true; - user.region_id = profile.region_id || null; - user.region_nom = profile.region_nom || null; - user.role = profile.role || user.role; - user.permissions = profile.permissions || user.permissions || {}; - } - } catch (_) { - // En cas d'échec, on garde le payload RPC minimal. - } - try { window.localStorage.setItem('app_user', JSON.stringify(user)); } catch (_) {} - onLogin(user); - } else { - setError("Identifiants invalides — merci de vérifier votre email et mot de passe"); + // Récupérer le profil métier depuis la table utilisateurs + const { data: profile, error: profileError } = await supabase + .from('utilisateurs') + .select('id, email, nom, prenom, role, actif, region_admin, region_id, region_nom, permissions') + .eq('email', email) + .maybeSingle(); + + if (profileError || !profile) { + setError("Profil utilisateur introuvable dans la base de données."); + await supabase.auth.signOut(); + setLoading(false); + return; } + + if (profile.actif === false) { + setError("Ce compte est désactivé."); + await supabase.auth.signOut(); + setLoading(false); + return; + } + + const user = { + id: profile.id, + email: profile.email, + nom: profile.nom, + prenom: profile.prenom, + name: [profile.nom, profile.prenom].filter(Boolean).join(' ').trim() || profile.email, + role: profile.role, + permissions: profile.permissions || {}, + region_admin: profile.region_admin === true, + region_id: profile.region_id || null, + region_nom: profile.region_nom || null + }; + + try { window.localStorage.setItem('app_user', JSON.stringify(user)); } catch (_) {} + onLogin(user); + } catch (err) { console.error('[Login] Unexpected error', err); // err peut contenir response / message diff --git a/src/supabaseClient.js b/src/supabaseClient.js index ede806f..9dffa4f 100644 --- a/src/supabaseClient.js +++ b/src/supabaseClient.js @@ -11,49 +11,7 @@ if (!isSupabaseConfigured) { console.warn('REACT_APP_SUPABASE_URL or REACT_APP_SUPABASE_ANON_KEY is not set. Supabase client will be initialized but requests may fail.'); } -// Nettoyer les clés résiduelles de sessions Supabase expirées dans le localStorage. -// L'application utilise une authentification personnalisée via RPC et non la session de base Supabase Auth. -// Avoir un ancien token de session expiré dans le localStorage force le client Supabase -// à l'envoyer dans l'en-tête Authorization, causant des erreurs 401 (Unauthorized) sur les requêtes d'écriture. -if (typeof window !== 'undefined' && window.localStorage) { - try { - const keysToRemove = []; - for (let i = 0; i < window.localStorage.length; i++) { - const key = window.localStorage.key(i); - if (key && (key.startsWith('sb-') || key.includes('supabase.auth.token'))) { - keysToRemove.push(key); - } - } - keysToRemove.forEach(key => window.localStorage.removeItem(key)); - } catch (e) { - console.warn('[SupabaseClient] Erreur lors du nettoyage du localStorage', e); - } -} - -const supabase = createClient(SUPABASE_URL || '', SUPABASE_ANON_KEY || '', { - auth: { - persistSession: false, - autoRefreshToken: false, - detectSessionInUrl: false - }, - global: { - fetch: (url, options) => { - // Forcer 'credentials: omit' pour empêcher le navigateur d'envoyer des cookies de session expirés - // (par exemple des cookies de sous-domaine .mr-patator.fr) qui déclencheraient des erreurs 401 de Supabase. - const newOptions = { ...options, credentials: 'omit' }; - return fetch(url, newOptions).then(async (response) => { - if (!response.ok) { - try { - const clone = response.clone(); - const bodyText = await clone.text(); - console.error('[Supabase] HTTP', response.status, response.statusText, bodyText); - } catch (e) {} - } - return response; - }); - } - } -}); +const supabase = createClient(SUPABASE_URL || '', SUPABASE_ANON_KEY || ''); export { isSupabaseConfigured }; export default supabase;