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
+69 -119

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 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();
}
};
}, []);
+15 -9
View File
@@ -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;
}
+37 -67
View File
@@ -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);
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
// Récupérer le profil métier depuis la table utilisateurs
const { data: profile, error: profileError } = 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 || {};
.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;
}
} catch (_) {
// En cas d'échec, on garde le payload RPC minimal.
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);
} else {
setError("Identifiants invalides — merci de vérifier votre email et mot de passe");
}
} catch (err) {
console.error('[Login] Unexpected error', err);
// 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.');
}
// 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;