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:
1 parent
d95924753c
commit
d89eaaa17f
4 files changed
+76
-126
No files matched your search
+16
@@ -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();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+44
-74
@@ -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
|
||||
|
||||
+1
-43
@@ -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;
|
||||
Reference in new issue
Block a user