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
+69
-119
No files matched your search
+16
@@ -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();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+37
-67
@@ -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.
|
|
||||||
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}`);
|
setError(`Erreur d'authentification: ${msg}`);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
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
|
|
||||||
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')
|
.from('utilisateurs')
|
||||||
.select('region_admin, region_id, region_nom, role, permissions')
|
.select('id, email, nom, prenom, role, actif, region_admin, region_id, region_nom, permissions')
|
||||||
.eq('id', user.id)
|
.eq('email', email)
|
||||||
.single();
|
.maybeSingle();
|
||||||
if (profile) {
|
|
||||||
user.region_admin = profile.region_admin === true;
|
if (profileError || !profile) {
|
||||||
user.region_id = profile.region_id || null;
|
setError("Profil utilisateur introuvable dans la base de données.");
|
||||||
user.region_nom = profile.region_nom || null;
|
await supabase.auth.signOut();
|
||||||
user.role = profile.role || user.role;
|
setLoading(false);
|
||||||
user.permissions = profile.permissions || user.permissions || {};
|
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 (_) {}
|
try { window.localStorage.setItem('app_user', JSON.stringify(user)); } catch (_) {}
|
||||||
onLogin(user);
|
onLogin(user);
|
||||||
} else {
|
|
||||||
setError("Identifiants invalides — merci de vérifier votre email et mot de passe");
|
|
||||||
}
|
|
||||||
} 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
@@ -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;
|
||||||
Reference in new issue
Block a user