ajout du dashboard
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 16s
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 16s
This commit is contained in:
+36
-12
@@ -1,29 +1,53 @@
|
||||
// src/App.jsx
|
||||
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 (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* === ROUTES PUBLIQUES (Utilisent le PublicLayout) === */}
|
||||
|
||||
{/* ========================================== */}
|
||||
{/* ZONE PUBLIQUE (Accès Libre) */}
|
||||
{/* ========================================== */}
|
||||
<Route element={<PublicLayout />}>
|
||||
<Route path="/" element={<Home />} />
|
||||
{/* <Route path="/offres" element={<Offres />} /> */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
{/* Tu pourras ajouter /offres, /register ici plus tard */}
|
||||
</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="/services" element={<Services />} /> */}
|
||||
</Route>
|
||||
|
||||
</Route>
|
||||
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ export default function PublicLayout() {
|
||||
return (
|
||||
<div style={{ backgroundColor: '#121212', color: '#E0E0E0', minHeight: '100vh', fontFamily: 'monospace' }}>
|
||||
<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>
|
||||
|
||||
{/* AJOUT DU BOUTON D'INSCRIPTION */}
|
||||
|
||||
+137
-136
@@ -1,162 +1,163 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getClientProfile, getClientOrders, getOrderService } from '../../services/api';
|
||||
// src/pages/app/Dashboard.jsx
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function Dashboard() {
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [orders, setOrders] = useState([]);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
}
|
||||
// --- DESIGN SYSTEM DU DASHBOARD ---
|
||||
const gridStyle = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||
gap: '25px',
|
||||
marginTop: '30px'
|
||||
};
|
||||
|
||||
fetchDashboardData();
|
||||
}, []);
|
||||
|
||||
const cardStyle = {
|
||||
backgroundColor: '#1A1A1A',
|
||||
border: '1px solid #333',
|
||||
padding: '20px',
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
backgroundColor: '#121212',
|
||||
border: '1px solid #222',
|
||||
borderTop: '4px solid #00E5FF',
|
||||
padding: '25px',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
const handleManageInstance = async (orderId) => {
|
||||
try {
|
||||
console.log(`[SYS] Demande d'accès au service #${orderId}...`);
|
||||
const serviceData = await getOrderService(orderId);
|
||||
const statusBadgeStyle = {
|
||||
position: 'absolute',
|
||||
top: '20px',
|
||||
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) {
|
||||
window.open(serviceData.server.login_url, '_blank');
|
||||
}
|
||||
else if (serviceData && serviceData.server) {
|
||||
console.log(serviceData);
|
||||
// L'URL d'action du formulaire de connexion HestiaCP
|
||||
const hestiaLoginUrl = "https://panel.gise.be/login/";
|
||||
const username = serviceData.username;
|
||||
const password = serviceData.password;
|
||||
|
||||
if (!username || !password) {
|
||||
alert("Identifiants introuvables. Le serveur est-il bien provisionné ?");
|
||||
return;
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
const btnStyle = {
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
marginTop: '20px',
|
||||
backgroundColor: 'transparent',
|
||||
color: '#00E5FF',
|
||||
border: '1px solid #00E5FF',
|
||||
textAlign: 'center',
|
||||
textDecoration: 'none',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: '0.8rem',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
boxSizing: 'border-box'
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '40px', color: '#E0E0E0', fontFamily: 'monospace', backgroundColor: '#121212', minHeight: '100vh' }}>
|
||||
<div style={{ fontFamily: 'monospace' }}>
|
||||
|
||||
{/* HEADER DU DASHBOARD */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #333', paddingBottom: '20px', marginBottom: '30px' }}>
|
||||
<h2 style={{ color: '#00E5FF', margin: 0 }}>CENTRE DE CONTRÔLE GISE</h2>
|
||||
{profile && (
|
||||
{/* --- EN-TÊTE DU TABLEAU DE BORD --- */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', borderBottom: '1px solid #222', paddingBottom: '20px' }}>
|
||||
<div>
|
||||
<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>Opérateur : <span style={{ color: '#FFF' }}>{profile.email}</span></div>
|
||||
<div>Crédits : <span style={{ color: '#00FF00' }}>{profile.balance} {profile.currency}</span></div>
|
||||
<div style={{ color: '#00FF66', fontSize: '0.85rem', marginBottom: '5px' }}>● SYSTÈME NOMINAL</div>
|
||||
<div style={{ color: '#555', fontSize: '0.75rem' }}>Dernière connexion : Aujourd'hui</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 && (
|
||||
<div style={{ backgroundColor: 'rgba(255, 68, 68, 0.1)', color: '#FF4444', padding: '15px', border: '1px solid #FF4444', marginBottom: '20px' }}>
|
||||
[ ERREUR ] : {error}
|
||||
{/* MODULE 1 : BARE-METAL (HESTIACP) */}
|
||||
<div style={cardStyle}>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* 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' }}>
|
||||
<strong style={{ color: '#FFF', fontSize: '1.1rem' }}>{order.title}</strong>
|
||||
<span style={{
|
||||
color: order.status === 'active' ? '#00FF00' : '#FF4444',
|
||||
fontSize: '0.8rem', textTransform: 'uppercase', border: `1px solid ${order.status === 'active' ? '#00FF00' : '#FF4444'}`, padding: '2px 6px'
|
||||
}}>
|
||||
{order.status}
|
||||
</span>
|
||||
<span>Bases SQL:</span> <span style={{ color: '#FFF' }}>2 / 10</span>
|
||||
</div>
|
||||
</div>
|
||||
<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'; }}
|
||||
onMouseOut={(e) => { e.target.style.backgroundColor = 'transparent'; e.target.style.color = '#00E5FF'; }}>
|
||||
Accéder au Panel
|
||||
</a>
|
||||
</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
|
||||
onClick={() => handleManageInstance(order.id)}
|
||||
style={{ marginTop: '10px', backgroundColor: 'transparent', color: '#00E5FF', border: '1px solid #00E5FF', padding: '8px', cursor: 'pointer', fontFamily: 'monospace' }}>
|
||||
GÉRER L'INSTANCE
|
||||
</button>
|
||||
{/* MODULE 2 : CLOUD (NEXTCLOUD) */}
|
||||
<div style={cardStyle}>
|
||||
<div style={statusBadgeStyle}>ACTIF</div>
|
||||
<h3 style={{ color: '#FFF', marginTop: 0, fontSize: '1.2rem' }}>// Sanctuaire Cloud</h3>
|
||||
<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 style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>Espace Utilisé:</span> <span style={{ color: '#FFF' }}>1.2 Go</span>
|
||||
</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' }}>>_ 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' }}>></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>
|
||||
);
|
||||
}
|
||||
+48
-21
@@ -1,6 +1,6 @@
|
||||
// src/pages/public/Login.jsx
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { loginClient } from '../../services/api';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
|
||||
export default function Login() {
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -10,57 +10,78 @@ export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogin = async (e) => {
|
||||
e.preventDefault(); // Empêche la page de se rafraîchir
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// On envoie la requête à FOSSBilling
|
||||
await loginClient(email, password);
|
||||
// 1. APPEL À TON API BACKEND (FOSSBilling / PHP)
|
||||
// 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 !
|
||||
// On redirige vers l'espace client sécurisé
|
||||
// --- SIMULATION D'AUTHENTIFICATION ---
|
||||
// (À 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');
|
||||
|
||||
} else {
|
||||
throw new Error("Accès refusé. Identifiants invalides ou signalement d'intrusion.");
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
setError(err.message || "Accès refusé. Vérifiez vos identifiants.");
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Styles CSS en variables pour garder le code lisible
|
||||
// --- DESIGN SYSTEM "BUNKER" ---
|
||||
const inputStyle = {
|
||||
width: '100%', padding: '12px', marginBottom: '15px',
|
||||
width: '100%', padding: '10px', marginBottom: '20px',
|
||||
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 = {
|
||||
width: '100%', padding: '12px', backgroundColor: loading ? '#333' : '#00E5FF',
|
||||
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 (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '10vh' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '10vh', padding: '0 20px' }}>
|
||||
<div style={{
|
||||
width: '100%', maxWidth: '400px', backgroundColor: '#242424',
|
||||
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' }}>
|
||||
Authentification
|
||||
<h2 style={{ color: '#FFF', textTransform: 'uppercase', marginBottom: '5px', fontSize: '1.5rem', textAlign: 'center' }}>
|
||||
Connexion au Nexus
|
||||
</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 && (
|
||||
<div style={{ backgroundColor: 'rgba(255, 68, 68, 0.1)', color: '#FF4444', padding: '10px', marginBottom: '15px', border: '1px solid #FF4444', fontSize: '0.9rem' }}>
|
||||
[ ERREUR ] : {error}
|
||||
<div style={{
|
||||
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>
|
||||
)}
|
||||
|
||||
<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
|
||||
type="email"
|
||||
value={email}
|
||||
@@ -70,7 +91,7 @@ export default function Login() {
|
||||
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
|
||||
type="password"
|
||||
value={password}
|
||||
@@ -81,9 +102,15 @@ export default function Login() {
|
||||
/>
|
||||
|
||||
<button type="submit" style={buttonStyle} disabled={loading}>
|
||||
{loading ? 'VÉRIFICATION...' : 'INITIALISER LA CONNEXION'}
|
||||
{loading ? 'VÉRIFICATION...' : 'OUVRIR LE SAS'}
|
||||
</button>
|
||||
</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 ></span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,21 @@ export default function Register() {
|
||||
e.preventDefault();
|
||||
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
|
||||
if (password !== confirmPassword) {
|
||||
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 ]
|
||||
</p>
|
||||
|
||||
{/* Affichage des alertes système */}
|
||||
{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' }}>
|
||||
[ ERREUR ] : {error}
|
||||
<div style={{
|
||||
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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user