RELEASE V.4.1.0 :

--> Refonte du système de login + Activation du compte (avec envoi d'un mail de confirmation).
This commit is contained in:
mathis committed 2026-09-30 21:57:38 +02:00
1 parent d95924753c
commit d89eaaa17f
4 files changed
+76 -126

No files matched your search

+16
View File
@@ -6,6 +6,8 @@ import ActiverCompte from './pages/activer-compte/ActiverCompte';
import PanelRouter from './pages/router/PanelRouter'; import PanelRouter from './pages/router/PanelRouter';
import AppHeader from './components/header/AppHeader'; import AppHeader from './components/header/AppHeader';
import supabase from './supabaseClient';
function App() { function App() {
const [currentUser, setCurrentUser] = useState(() => { const [currentUser, setCurrentUser] = useState(() => {
try { try {
@@ -52,10 +54,24 @@ function App() {
window.addEventListener('app:authChanged', onAuthChanged); window.addEventListener('app:authChanged', onAuthChanged);
window.addEventListener('unhandledrejection', onUnhandledRejection); 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 // cleanup
return () => { return () => {
window.removeEventListener('app:authChanged', onAuthChanged); window.removeEventListener('app:authChanged', onAuthChanged);
window.removeEventListener('unhandledrejection', onUnhandledRejection); window.removeEventListener('unhandledrejection', onUnhandledRejection);
if (authListener && authListener.subscription) {
authListener.subscription.unsubscribe();
}
}; };
}, []); }, []);
+15 -9
View File
@@ -119,25 +119,31 @@ export default function ActiverCompte() {
setLoading(true); setLoading(true);
try { try {
const { hash } = await import('bcryptjs'); // 1. Création du compte dans Supabase Auth (GoTrue)
const passwordHash = await hash(password, 10); 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 const { error: activationError } = await supabase
.from('utilisateurs') .from('utilisateurs')
.update({ .update({
password_hash: passwordHash, password_hash: null, // On vide l'ancien hash s'il y en avait un
actif: true, actif: true,
updated_at: new Date().toISOString() updated_at: new Date().toISOString()
}) })
.eq('id', account.id); .eq('id', account.id);
if (activationError) { if (activationError) {
await supabase setError("Le compte Auth a été créé mais l'activation du profil a échoué.");
.from('utilisateurs')
.update({ actif: false, updated_at: new Date().toISOString() })
.eq('id', account.id);
setError('Activation impossible. L\'acces au compte a ete desactive.');
return; return;
} }
+44 -74
View File
@@ -3,38 +3,6 @@ import supabase, { isSupabaseConfigured } from '../../supabaseClient';
import './Login.css'; 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 }) { export default function Login({ onLogin }) {
const emailRef = useRef(null); const emailRef = useRef(null);
@@ -80,53 +48,55 @@ export default function Login({ onLogin }) {
} }
try { try {
const { data, error: rpcError } = await supabase.rpc('authenticate_user', { const { data: authData, error: authError } = await supabase.auth.signInWithPassword({
p_email: email, email,
p_password: password password
}); });
let effectiveData = data; if (authError || !authData.user) {
if (rpcError || !((Array.isArray(data) && data.length) || (!Array.isArray(data) && data))) { const msg = authError?.message || 'Identifiants invalides';
// Fallback de secours si la RPC est absente/mal configurée. setError(`Erreur d'authentification: ${msg}`);
const fallbackUser = await tryFallbackAuth(email, password); setLoading(false);
if (fallbackUser) { return;
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;
}
} }
const user = Array.isArray(effectiveData) ? (effectiveData.length ? effectiveData[0] : null) : effectiveData; // Récupérer le profil métier depuis la table utilisateurs
if (user && user.id) { const { data: profile, error: profileError } = await supabase
// S'assurer que permissions est au moins un objet vide pour que le RBAC applique les templates de base .from('utilisateurs')
user.permissions = user.permissions || {}; .select('id, email, nom, prenom, role, actif, region_admin, region_id, region_nom, permissions')
.eq('email', email)
// Complète le profil avec les infos régionales nécessaires au RBAC front. .maybeSingle();
try {
const { data: profile } = await supabase if (profileError || !profile) {
.from('utilisateurs') setError("Profil utilisateur introuvable dans la base de données.");
.select('region_admin, region_id, region_nom, role, permissions') await supabase.auth.signOut();
.eq('id', user.id) setLoading(false);
.single(); return;
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");
} }
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) { } catch (err) {
console.error('[Login] Unexpected error', err); console.error('[Login] Unexpected error', err);
// err peut contenir response / message // err peut contenir response / message
+1 -43
View File
@@ -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.'); 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. const supabase = createClient(SUPABASE_URL || '', SUPABASE_ANON_KEY || '');
// 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;
});
}
}
});
export { isSupabaseConfigured }; export { isSupabaseConfigured };
export default supabase; export default supabase;