ajout du dashboard
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 16s

This commit is contained in:
2026-06-12 15:53:16 +02:00
parent cab5d749d2
commit bfa3161646
10 changed files with 400 additions and 195 deletions
+36 -12
View File
@@ -1,29 +1,53 @@
// src/App.jsx // src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { BrowserRouter, Routes, Route } from 'react-router-dom';
import PublicLayout from './layouts/PublicLayout';
import Home from './pages/public/Home';
import Login from './pages/public/Login';
import Dashboard from './pages/app/Dashboard';
import Register from './pages/public/Register';
function App() { // Import des Layouts
import PublicLayout from './layouts/PublicLayout';
import AppLayout from './layouts/AppLayout';
// Import du Garde du Corps
import ProtectedRoute from './components/ProtectedRoute';
// Import des Pages Publiques
import Register from './pages/public/Register';
import Login from './pages/public/Login';
import Home from './pages/public/Home';
//import Offres from './pages/public/Offres';
// Import des Pages Privées (Espace Client)
import Dashboard from './pages/app/Dashboard';
//import Services from './pages/app/Services';
export default function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
{/* === ROUTES PUBLIQUES (Utilisent le PublicLayout) === */}
{/* ========================================== */}
{/* ZONE PUBLIQUE (Accès Libre) */}
{/* ========================================== */}
<Route element={<PublicLayout />}> <Route element={<PublicLayout />}>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
{/* <Route path="/offres" element={<Offres />} /> */}
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} /> <Route path="/register" element={<Register />} />
{/* Tu pourras ajouter /offres, /register ici plus tard */}
</Route> </Route>
{/* === ROUTES PRIVÉES (Espace Client) === */} {/* ========================================== */}
{/* Pour l'instant on les met à nu, on créera un AppLayout et un système de sécurité plus tard */} {/* ZONE PRIVÉE SÉCURISÉE (Le Bunker) */}
{/* ========================================== */}
{/* Le Garde du corps bloque l'entrée ici */}
<Route element={<ProtectedRoute />}>
{/* Si autorisé, on charge l'interface avec la Sidebar */}
<Route element={<AppLayout />}>
<Route path="/dashboard" element={<Dashboard />} /> <Route path="/dashboard" element={<Dashboard />} />
{/* <Route path="/services" element={<Services />} /> */}
</Route>
</Route>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
); );
} }
export default App;
+19
View File
@@ -0,0 +1,19 @@
// src/components/ProtectedRoute.jsx
import { Navigate, Outlet } from 'react-router-dom';
export default function ProtectedRoute() {
// 1. VÉRIFICATION DU BADGE D'ACCÈS
// Pour l'instant, on regarde si un jeton "gise_token" existe dans le navigateur.
// (Lors de ta vraie fonction de connexion, tu enregistreras le token de FOSSBilling ici)
const isAuthenticated = localStorage.getItem('gise_token');
// 2. DÉCISION DE SÉCURITÉ
if (!isAuthenticated) {
// ALERTE INTRUSION : On renvoie l'utilisateur vers la porte d'entrée
// Le "replace" efface la tentative de l'historique du navigateur
return <Navigate to="/login" replace />;
}
// ACCÈS AUTORISÉ : On affiche les routes enfants (Le Dashboard, l'AppLayout...)
return <Outlet />;
}
+111
View File
@@ -0,0 +1,111 @@
// src/layouts/AppLayout.jsx
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
export default function AppLayout() {
const navigate = useNavigate();
const handleLogout = () => {
// Ici, tu pourras ajouter la logique de déconnexion (effacer les cookies/tokens)
alert("[ DÉCONNEXION EN COURS... ]");
navigate('/login');
};
// --- DESIGN SYSTEM DU BUNKER ---
const sidebarStyle = {
width: '260px',
backgroundColor: '#121212', // Gris blindage
borderRight: '1px solid #222222',
height: '100vh',
display: 'flex',
flexDirection: 'column',
position: 'fixed',
top: 0,
left: 0,
fontFamily: 'monospace'
};
const mainContentStyle = {
marginLeft: '260px', // Laisse la place à la sidebar
minHeight: '100vh',
backgroundColor: '#050505', // Fond abyssal
color: '#E0E0E0',
padding: '40px'
};
// Fonction pour styliser le lien actif (en Cyan)
const getNavLinkStyle = ({ isActive }) => ({
display: 'block',
padding: '15px 25px',
color: isActive ? '#00E5FF' : '#888',
backgroundColor: isActive ? 'rgba(0, 229, 255, 0.05)' : 'transparent',
textDecoration: 'none',
borderLeft: isActive ? '4px solid #00E5FF' : '4px solid transparent',
transition: 'all 0.2s',
textTransform: 'uppercase',
letterSpacing: '1px',
fontSize: '0.9rem'
});
return (
<div style={{ display: 'flex' }}>
{/* ========================================== */}
{/* BARRE LATÉRALE (SIDEBAR) */}
{/* ========================================== */}
<aside style={sidebarStyle}>
{/* En-tête de la Sidebar (Logo / Marque) */}
<div style={{ padding: '30px 25px', borderBottom: '1px solid #222' }}>
<h1 style={{ color: '#00E5FF', margin: 0, fontSize: '1.5rem', letterSpacing: '2px' }}>
GISE<span style={{ color: '#FFF' }}>_NEXUS</span>
</h1>
<p style={{ color: '#555', fontSize: '0.7rem', margin: '5px 0 0 0' }}>
CONSOLE D'INFRASTRUCTURE v1.0
</p>
</div>
{/* Menu de Navigation */}
<nav style={{ flex: 1, marginTop: '20px' }}>
<div style={{ padding: '0 25px', marginBottom: '10px', color: '#444', fontSize: '0.7rem', fontWeight: 'bold' }}>
// SUPERVISION
</div>
<NavLink style={getNavLinkStyle} to="/dashboard">Tableau de bord</NavLink>
<NavLink style={getNavLinkStyle} to="/services">Inventaire Réseau</NavLink>
<div style={{ padding: '0 25px', marginTop: '25px', marginBottom: '10px', color: '#444', fontSize: '0.7rem', fontWeight: 'bold' }}>
// GESTION
</div>
<NavLink style={getNavLinkStyle} to="/billing">Facturation</NavLink>
<NavLink style={getNavLinkStyle} to="/support">Tickets Support</NavLink>
<NavLink style={getNavLinkStyle} to="/settings">Profil & Sécurité</NavLink>
</nav>
{/* Bas de la Sidebar (Déconnexion) */}
<div style={{ padding: '20px', borderTop: '1px solid #222' }}>
<button
onClick={handleLogout}
style={{
width: '100%', padding: '12px', backgroundColor: 'transparent',
color: '#ff003c', border: '1px solid #ff003c', cursor: 'pointer',
fontFamily: 'monospace', textTransform: 'uppercase'
}}
>
[ Déconnexion ]
</button>
</div>
</aside>
{/* ========================================== */}
{/* ZONE DE CONTENU DYNAMIQUE */}
{/* ========================================== */}
<main style={mainContentStyle}>
{/* Le composant <Outlet /> est magique :
C'est ici que React Router va injecter le contenu de la page demandée
(Dashboard, Settings, etc.) sans jamais recharger la barre latérale !
*/}
<Outlet />
</main>
</div>
);
}
+1 -1
View File
@@ -5,7 +5,7 @@ export default function PublicLayout() {
return ( return (
<div style={{ backgroundColor: '#121212', color: '#E0E0E0', minHeight: '100vh', fontFamily: 'monospace' }}> <div style={{ backgroundColor: '#121212', color: '#E0E0E0', minHeight: '100vh', fontFamily: 'monospace' }}>
<nav style={{ padding: '20px', borderBottom: '1px solid #242424', display: 'flex', gap: '15px', alignItems: 'center' }}> <nav style={{ padding: '20px', borderBottom: '1px solid #242424', display: 'flex', gap: '15px', alignItems: 'center' }}>
<Link to="/" style={{ color: '#00E5FF', textDecoration: 'none', fontWeight: 'bold' }}>[ GISE_BUNKER ]</Link> <Link to="/" style={{ color: '#00E5FF', textDecoration: 'none', fontWeight: 'bold' }}>[ GISE_NEXUS ]</Link>
<Link to="/offres" style={{ color: '#E0E0E0', textDecoration: 'none' }}>Catalogue</Link> <Link to="/offres" style={{ color: '#E0E0E0', textDecoration: 'none' }}>Catalogue</Link>
{/* AJOUT DU BOUTON D'INSCRIPTION */} {/* AJOUT DU BOUTON D'INSCRIPTION */}
+137 -136
View File
@@ -1,162 +1,163 @@
import { useState, useEffect } from 'react'; // src/pages/app/Dashboard.jsx
import { getClientProfile, getClientOrders, getOrderService } from '../../services/api'; import { Link } from 'react-router-dom';
export default function Dashboard() { export default function Dashboard() {
const [profile, setProfile] = useState(null); // --- DESIGN SYSTEM DU DASHBOARD ---
const [orders, setOrders] = useState([]); const gridStyle = {
const [error, setError] = useState(null); display: 'grid',
const [loading, setLoading] = useState(true); gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '25px',
useEffect(() => { marginTop: '30px'
const fetchDashboardData = async () => {
try {
// On lance les deux requêtes en même temps pour aller plus vite
const profileData = await getClientProfile();
const ordersData = await getClientOrders();
setProfile(profileData);
// FOSSBilling renvoie une liste paginée, on prend le tableau 'list'
setOrders(ordersData.list || []);
} catch (err) {
setError(err.message || "Erreur de synchronisation avec le serveur central.");
} finally {
setLoading(false);
}
}; };
fetchDashboardData();
}, []);
const cardStyle = { const cardStyle = {
backgroundColor: '#1A1A1A', backgroundColor: '#121212',
border: '1px solid #333', border: '1px solid #222',
padding: '20px', borderTop: '4px solid #00E5FF',
borderRadius: '4px', padding: '25px',
display: 'flex', position: 'relative',
flexDirection: 'column', overflow: 'hidden'
gap: '10px'
}; };
const handleManageInstance = async (orderId) => { const statusBadgeStyle = {
try { position: 'absolute',
console.log(`[SYS] Demande d'accès au service #${orderId}...`); top: '20px',
const serviceData = await getOrderService(orderId); right: '20px',
backgroundColor: 'rgba(0, 229, 255, 0.1)',
color: '#00E5FF',
padding: '4px 8px',
fontSize: '0.7rem',
fontWeight: 'bold',
letterSpacing: '1px'
};
if (serviceData && serviceData.server && serviceData.server.login_url) { const btnStyle = {
window.open(serviceData.server.login_url, '_blank'); display: 'inline-block',
} width: '100%',
else if (serviceData && serviceData.server) { padding: '10px',
console.log(serviceData); marginTop: '20px',
// L'URL d'action du formulaire de connexion HestiaCP backgroundColor: 'transparent',
const hestiaLoginUrl = "https://panel.gise.be/login/"; color: '#00E5FF',
const username = serviceData.username; border: '1px solid #00E5FF',
const password = serviceData.password; textAlign: 'center',
textDecoration: 'none',
if (!username || !password) { textTransform: 'uppercase',
alert("Identifiants introuvables. Le serveur est-il bien provisionné ?"); fontSize: '0.8rem',
return; cursor: 'pointer',
} transition: 'all 0.2s',
boxSizing: 'border-box'
console.log("[SYS] Création du pont SSO vers HestiaCP...");
// 1. On crée un formulaire invisible
const form = document.createElement('form');
form.method = 'POST';
form.action = hestiaLoginUrl;
form.target = '_blank'; // Pour ouvrir dans un nouvel onglet
// 2. On crée le champ utilisateur (Hestia attend le nom 'user')
const userField = document.createElement('input');
userField.type = 'hidden';
userField.name = 'user';
userField.value = username;
// 3. On crée le champ mot de passe (Hestia attend le nom 'password')
const passField = document.createElement('input');
passField.type = 'hidden';
passField.name = 'password';
passField.value = password;
// 4. On assemble et on injecte dans la page
form.appendChild(userField);
form.appendChild(passField);
document.body.appendChild(form);
// 5. On valide le formulaire (BAM ! Connexion)
form.submit();
// 6. On efface les traces du formulaire fantôme pour la sécurité
document.body.removeChild(form);
} else {
alert("Configuration serveur introuvable pour cette instance.");
}
} catch (err) {
alert(`[ERREUR D'ACCÈS] : ${err.message}`);
}
}; };
return ( return (
<div style={{ padding: '40px', color: '#E0E0E0', fontFamily: 'monospace', backgroundColor: '#121212', minHeight: '100vh' }}> <div style={{ fontFamily: 'monospace' }}>
{/* HEADER DU DASHBOARD */} {/* --- EN-TÊTE DU TABLEAU DE BORD --- */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #333', paddingBottom: '20px', marginBottom: '30px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', borderBottom: '1px solid #222', paddingBottom: '20px' }}>
<h2 style={{ color: '#00E5FF', margin: 0 }}>CENTRE DE CONTRÔLE GISE</h2> <div>
{profile && ( <h2 style={{ color: '#FFF', margin: '0 0 5px 0', fontSize: '2rem', textTransform: 'uppercase' }}>
Console Opérationnelle
</h2>
<span style={{ color: '#888' }}>ID Réseau: <span style={{ color: '#E0E0E0' }}>usr_8472_alpha</span></span>
</div>
<div style={{ textAlign: 'right' }}> <div style={{ textAlign: 'right' }}>
<div>Opérateur : <span style={{ color: '#FFF' }}>{profile.email}</span></div> <div style={{ color: '#00FF66', fontSize: '0.85rem', marginBottom: '5px' }}> SYSTÈME NOMINAL</div>
<div>Crédits : <span style={{ color: '#00FF00' }}>{profile.balance} {profile.currency}</span></div> <div style={{ color: '#555', fontSize: '0.75rem' }}>Dernière connexion : Aujourd'hui</div>
</div> </div>
)}
</div> </div>
{loading && <p style={{ color: '#888' }}>[ Synchronisation des données en cours... ]</p>} {/* --- GRILLE DES MODULES DE L'INFRASTRUCTURE --- */}
<div style={gridStyle}>
{error && ( {/* MODULE 1 : BARE-METAL (HESTIACP) */}
<div style={{ backgroundColor: 'rgba(255, 68, 68, 0.1)', color: '#FF4444', padding: '15px', border: '1px solid #FF4444', marginBottom: '20px' }}> <div style={cardStyle}>
[ ERREUR ] : {error} <div style={statusBadgeStyle}>ACTIF</div>
<h3 style={{ color: '#FFF', marginTop: 0, fontSize: '1.2rem' }}>// Serveur Web</h3>
<p style={{ color: '#888', fontSize: '0.85rem', lineHeight: '1.5' }}>
Nœud HestiaCP (panel.gise.be). Supervision des partitions web, DNS et bases de données.
</p>
<div style={{ margin: '20px 0', fontSize: '0.85rem', color: '#A0A0A0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span>Domaines Actifs:</span> <span style={{ color: '#FFF' }}>1 / 5</span>
</div> </div>
)}
{/* ZONE DES SERVICES ACTIFS */}
{!loading && !error && (
<>
<h3 style={{ color: '#FFF', marginBottom: '20px' }}>INFRASTRUCTURE ACTIVE</h3>
{orders.length === 0 ? (
<div style={{ padding: '30px', textAlign: 'center', border: '1px dashed #333', color: '#888' }}>
Aucun service actif détecté. <br/><br/>
<button style={{ backgroundColor: '#00E5FF', color: '#000', border: 'none', padding: '10px 20px', cursor: 'pointer', fontFamily: 'monospace', fontWeight: 'bold' }}>
+ DÉPLOYER UN NOUVEAU SERVICE
</button>
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '20px' }}>
{orders.map((order) => (
<div key={order.id} style={cardStyle}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<strong style={{ color: '#FFF', fontSize: '1.1rem' }}>{order.title}</strong> <span>Bases SQL:</span> <span style={{ color: '#FFF' }}>2 / 10</span>
<span style={{ </div>
color: order.status === 'active' ? '#00FF00' : '#FF4444', </div>
fontSize: '0.8rem', textTransform: 'uppercase', border: `1px solid ${order.status === 'active' ? '#00FF00' : '#FF4444'}`, padding: '2px 6px' <a href="https://panel.gise.be" target="_blank" rel="noopener noreferrer" style={btnStyle}
}}> onMouseOver={(e) => { e.target.style.backgroundColor = '#00E5FF'; e.target.style.color = '#000'; }}
{order.status} onMouseOut={(e) => { e.target.style.backgroundColor = 'transparent'; e.target.style.color = '#00E5FF'; }}>
</span> Accéder au Panel
</a>
</div> </div>
<div style={{ fontSize: '0.9rem', color: '#A0A0A0' }}>Renouvellement : {order.expires_at || 'N/A'}</div>
<div style={{ fontSize: '0.9rem', color: '#A0A0A0' }}>Montant : {order.total} {order.currency}</div>
<button {/* MODULE 2 : CLOUD (NEXTCLOUD) */}
onClick={() => handleManageInstance(order.id)} <div style={cardStyle}>
style={{ marginTop: '10px', backgroundColor: 'transparent', color: '#00E5FF', border: '1px solid #00E5FF', padding: '8px', cursor: 'pointer', fontFamily: 'monospace' }}> <div style={statusBadgeStyle}>ACTIF</div>
GÉRER L'INSTANCE <h3 style={{ color: '#FFF', marginTop: 0, fontSize: '1.2rem' }}>// Sanctuaire Cloud</h3>
</button> <p style={{ color: '#888', fontSize: '0.85rem', lineHeight: '1.5' }}>
Stockage chiffré Nextcloud (cloud.gise.be). Synchronisation des terminaux et partage sécurisé.
</p>
<div style={{ margin: '20px 0', fontSize: '0.85rem', color: '#A0A0A0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span>Espace Alloué:</span> <span style={{ color: '#FFF' }}>50 Go</span>
</div> </div>
))} <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>Espace Utilisé:</span> <span style={{ color: '#FFF' }}>1.2 Go</span>
</div> </div>
)} </div>
</> <a href="https://cloud.gise.be" target="_blank" rel="noopener noreferrer" style={btnStyle}
)} onMouseOver={(e) => { e.target.style.backgroundColor = '#00E5FF'; e.target.style.color = '#000'; }}
onMouseOut={(e) => { e.target.style.backgroundColor = 'transparent'; e.target.style.color = '#00E5FF'; }}>
Ouvrir le Cloud
</a>
</div>
{/* MODULE 3 : FACTURATION (FOSSBILLING) */}
<div style={cardStyle}>
<div style={statusBadgeStyle} style={{ ...statusBadgeStyle, color: '#FFB800', backgroundColor: 'rgba(255, 184, 0, 0.1)' }}>EN ATTENTE</div>
<h3 style={{ color: '#FFF', marginTop: 0, fontSize: '1.2rem' }}>// Facturation</h3>
<p style={{ color: '#888', fontSize: '0.85rem', lineHeight: '1.5' }}>
Centre de gestion FOSSBilling. Historique des paiements et renouvellement des baux réseau.
</p>
<div style={{ margin: '20px 0', fontSize: '0.85rem', color: '#A0A0A0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span>Solde Compte:</span> <span style={{ color: '#FFF' }}>0.00 </span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>Prochaine Échéance:</span> <span style={{ color: '#FFF' }}>12/07/2026</span>
</div>
</div>
<Link to="/billing" style={{ ...btnStyle, borderColor: '#FFB800', color: '#FFB800' }}
onMouseOver={(e) => { e.target.style.backgroundColor = '#FFB800'; e.target.style.color = '#000'; }}
onMouseOut={(e) => { e.target.style.backgroundColor = 'transparent'; e.target.style.color = '#FFB800'; }}>
Régler la facture
</Link>
</div>
</div>
{/* --- TERMINAL DE LOGS (ESTHÉTIQUE SYSADMIN) --- */}
<div style={{ marginTop: '40px', backgroundColor: '#0A0A0A', border: '1px solid #1A1A1A', padding: '20px' }}>
<h4 style={{ color: '#555', marginTop: 0, fontSize: '0.8rem', letterSpacing: '1px' }}>&gt;_ SYSTEM_LOGS</h4>
<div style={{ color: '#444', fontSize: '0.75rem', lineHeight: '1.8' }}>
<div>[ OK ] Connexion chiffrée établie via TLS v1.3</div>
<div>[ OK ] Jetons d'authentification synchronisés avec FOSSBilling API</div>
<div>[ INFO ] Vérification des quotas de stockage Nextcloud... Terminée.</div>
<div>[ INFO ] 0 ticket(s) de support en attente.</div>
<div><span style={{ color: '#00E5FF' }}>&gt;</span> En attente d'instructions...<span style={{ animation: 'blink 1s step-end infinite' }}>_</span></div>
</div>
</div>
{/* Animation CSS pour le curseur clignotant du terminal */}
<style>
{`
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
`}
</style>
</div> </div>
); );
} }
View File
+48 -21
View File
@@ -1,6 +1,6 @@
// src/pages/public/Login.jsx
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate, Link } from 'react-router-dom';
import { loginClient } from '../../services/api';
export default function Login() { export default function Login() {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@@ -10,57 +10,78 @@ export default function Login() {
const navigate = useNavigate(); const navigate = useNavigate();
const handleLogin = async (e) => { const handleLogin = async (e) => {
e.preventDefault(); // Empêche la page de se rafraîchir e.preventDefault();
setError(null); setError(null);
setLoading(true); setLoading(true);
try { try {
// On envoie la requête à FOSSBilling // 1. APPEL À TON API BACKEND (FOSSBilling / PHP)
await loginClient(email, password); // Ici, tu mettras ton vrai 'fetch' vers ton serveur pour vérifier le mot de passe.
// Pour l'instant, on simule un délai réseau d'une seconde.
await new Promise(resolve => setTimeout(resolve, 1000));
// Si on arrive ici, le login est un succès ! // --- SIMULATION D'AUTHENTIFICATION ---
// On redirige vers l'espace client sécurisé // remplacer par la vraie validation de ton serveur)
if (email === 'test@gise.be' && password === 'Bunker!2026') {
// 2. LA CLÉ DU PROBLÈME EST ICI : L'ATTRIBUTION DU BADGE
// On sauvegarde le token (généralement renvoyé par ton API) dans le navigateur
localStorage.setItem('gise_token', 'secure_token_alphanumerique_factice');
// 3. AUTORISATION ET REDIRECTION
// Maintenant que le token est en poche, ProtectedRoute nous laissera passer !
navigate('/dashboard'); navigate('/dashboard');
} else {
throw new Error("Accès refusé. Identifiants invalides ou signalement d'intrusion.");
}
} catch (err) { } catch (err) {
setError(err.message || "Accès refusé. Vérifiez vos identifiants."); setError(err.message);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
// Styles CSS en variables pour garder le code lisible // --- DESIGN SYSTEM "BUNKER" ---
const inputStyle = { const inputStyle = {
width: '100%', padding: '12px', marginBottom: '15px', width: '100%', padding: '10px', marginBottom: '20px',
backgroundColor: '#1A1A1A', color: '#00E5FF', backgroundColor: '#1A1A1A', color: '#00E5FF',
border: '1px solid #333', fontFamily: 'monospace', outline: 'none' border: '1px solid #333', fontFamily: 'monospace', outline: 'none',
boxSizing: 'border-box'
}; };
const buttonStyle = { const buttonStyle = {
width: '100%', padding: '12px', backgroundColor: loading ? '#333' : '#00E5FF', width: '100%', padding: '12px', backgroundColor: loading ? '#333' : '#00E5FF',
color: loading ? '#888' : '#000', border: 'none', cursor: loading ? 'not-allowed' : 'pointer', color: loading ? '#888' : '#000', border: 'none', cursor: loading ? 'not-allowed' : 'pointer',
fontFamily: 'monospace', fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: '1px' fontFamily: 'monospace', fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: '1px',
marginTop: '10px'
}; };
return ( return (
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '10vh' }}> <div style={{ display: 'flex', justifyContent: 'center', marginTop: '10vh', padding: '0 20px' }}>
<div style={{ <div style={{
width: '100%', maxWidth: '400px', backgroundColor: '#242424', width: '100%', maxWidth: '400px', backgroundColor: '#242424',
padding: '30px', borderTop: '4px solid #00E5FF', boxShadow: '0 10px 30px rgba(0,0,0,0.5)' padding: '30px', borderTop: '4px solid #00E5FF', boxShadow: '0 10px 30px rgba(0,0,0,0.5)'
}}> }}>
<h2 style={{ color: '#FFF', textTransform: 'uppercase', marginBottom: '20px', fontSize: '1.5rem' }}> <h2 style={{ color: '#FFF', textTransform: 'uppercase', marginBottom: '5px', fontSize: '1.5rem', textAlign: 'center' }}>
Authentification Connexion au Nexus
</h2> </h2>
<p style={{ color: '#888', fontFamily: 'monospace', fontSize: '0.85rem', marginBottom: '25px', textAlign: 'center' }}>
[ IDENTIFICATION REQUISE ]
</p>
{/* Zone d'erreur qui s'affiche si le mot de passe est faux */}
{error && ( {error && (
<div style={{ backgroundColor: 'rgba(255, 68, 68, 0.1)', color: '#FF4444', padding: '10px', marginBottom: '15px', border: '1px solid #FF4444', fontSize: '0.9rem' }}> <div style={{
[ ERREUR ] : {error} color: '#ff003c', border: '1px solid #ff003c', backgroundColor: 'rgba(255, 0, 60, 0.1)',
padding: '10px', marginBottom: '20px', fontFamily: 'monospace', fontSize: '0.85rem'
}}>
[ ALERTE ] : {error}
</div> </div>
)} )}
<form onSubmit={handleLogin}> <form onSubmit={handleLogin}>
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>IDENTIFIANT (EMAIL)</label> <label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>IDENTIFIANT (E-MAIL)</label>
<input <input
type="email" type="email"
value={email} value={email}
@@ -70,7 +91,7 @@ export default function Login() {
placeholder="admin@gise.be" placeholder="admin@gise.be"
/> />
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>CLÉ D'ACCÈS (MOT DE PASSE)</label> <label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>CLÉ D'ACCÈS SÉCURISÉE</label>
<input <input
type="password" type="password"
value={password} value={password}
@@ -81,9 +102,15 @@ export default function Login() {
/> />
<button type="submit" style={buttonStyle} disabled={loading}> <button type="submit" style={buttonStyle} disabled={loading}>
{loading ? 'VÉRIFICATION...' : 'INITIALISER LA CONNEXION'} {loading ? 'VÉRIFICATION...' : 'OUVRIR LE SAS'}
</button> </button>
</form> </form>
<div style={{ marginTop: '20px', textAlign: 'center', fontSize: '0.85rem' }}>
<Link to="/register" style={{ color: '#888', textDecoration: 'none' }}>
Aucun accès réseau ? <span style={{ color: '#00E5FF' }}>S'enregistrer &gt;</span>
</Link>
</div>
</div> </div>
</div> </div>
); );
View File
+25 -2
View File
@@ -18,6 +18,21 @@ export default function Register() {
e.preventDefault(); e.preventDefault();
setError(null); setError(null);
// 1. DÉFINITION DE LA POLITIQUE DE MOT DE PASSE (Le Checkpoint)
// Explication de la Regex :
// (?=.*[a-z]) : Au moins une minuscule
// (?=.*[A-Z]) : Au moins une majuscule
// (?=.*\d) : Au moins un chiffre
// (?=.*[\W_]) : Au moins un caractère spécial (non-alphanumérique ou underscore)
// .{8,} : Minimum 8 caractères au total
const passwordPolicy = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/;
// 2. VÉRIFICATION
if (!passwordPolicy.test(password)) {
setError("ERREUR : Le mot de passe doit contenir 8 caractères min, une majuscule, une minuscule, un chiffre et un caractère spécial.");
return; // On stoppe l'exécution ici. La requête ne part pas vers le serveur.
}
// Sécurité Frontend : Validation des mots de passe avant envoi au serveur // Sécurité Frontend : Validation des mots de passe avant envoi au serveur
if (password !== confirmPassword) { if (password !== confirmPassword) {
setError("Les clés d'accès (mots de passe) ne correspondent pas."); setError("Les clés d'accès (mots de passe) ne correspondent pas.");
@@ -76,9 +91,17 @@ export default function Register() {
[ INITIALISATION DU PROVISIONNEMENT TRIPLE EN CASCADE ] [ INITIALISATION DU PROVISIONNEMENT TRIPLE EN CASCADE ]
</p> </p>
{/* Affichage des alertes système */}
{error && ( {error && (
<div style={{ backgroundColor: 'rgba(255, 68, 68, 0.1)', color: '#FF4444', padding: '10px', marginBottom: '20px', border: '1px solid #FF4444', fontSize: '0.9rem', fontFamily: 'monospace' }}> <div style={{
[ ERREUR ] : {error} color: '#ff003c', // Un rouge néon agressif pour les erreurs
border: '1px solid #ff003c',
backgroundColor: 'rgba(255, 0, 60, 0.1)',
padding: '10px',
marginBottom: '15px',
fontFamily: 'monospace'
}}>
[ ALERTE SYSTÈME ] : {error}
</div> </div>
)} )}