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:
+38
-14
@@ -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) */}
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
{/* ========================================== */}
|
||||||
|
{/* 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>
|
</Routes>
|
||||||
</BrowserRouter>
|
</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 (
|
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 */}
|
||||||
|
|||||||
+143
-142
@@ -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 = {
|
|
||||||
backgroundColor: '#1A1A1A',
|
|
||||||
border: '1px solid #333',
|
|
||||||
padding: '20px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '10px'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleManageInstance = async (orderId) => {
|
const cardStyle = {
|
||||||
try {
|
backgroundColor: '#121212',
|
||||||
console.log(`[SYS] Demande d'accès au service #${orderId}...`);
|
border: '1px solid #222',
|
||||||
const serviceData = await getOrderService(orderId);
|
borderTop: '4px solid #00E5FF',
|
||||||
|
padding: '25px',
|
||||||
if (serviceData && serviceData.server && serviceData.server.login_url) {
|
position: 'relative',
|
||||||
window.open(serviceData.server.login_url, '_blank');
|
overflow: 'hidden'
|
||||||
}
|
};
|
||||||
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...");
|
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'
|
||||||
|
};
|
||||||
|
|
||||||
// 1. On crée un formulaire invisible
|
const btnStyle = {
|
||||||
const form = document.createElement('form');
|
display: 'inline-block',
|
||||||
form.method = 'POST';
|
width: '100%',
|
||||||
form.action = hestiaLoginUrl;
|
padding: '10px',
|
||||||
form.target = '_blank'; // Pour ouvrir dans un nouvel onglet
|
marginTop: '20px',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
// 2. On crée le champ utilisateur (Hestia attend le nom 'user')
|
color: '#00E5FF',
|
||||||
const userField = document.createElement('input');
|
border: '1px solid #00E5FF',
|
||||||
userField.type = 'hidden';
|
textAlign: 'center',
|
||||||
userField.name = 'user';
|
textDecoration: 'none',
|
||||||
userField.value = username;
|
textTransform: 'uppercase',
|
||||||
|
fontSize: '0.8rem',
|
||||||
// 3. On crée le champ mot de passe (Hestia attend le nom 'password')
|
cursor: 'pointer',
|
||||||
const passField = document.createElement('input');
|
transition: 'all 0.2s',
|
||||||
passField.type = 'hidden';
|
boxSizing: 'border-box'
|
||||||
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' }}>
|
||||||
<div style={{ textAlign: 'right' }}>
|
Console Opérationnelle
|
||||||
<div>Opérateur : <span style={{ color: '#FFF' }}>{profile.email}</span></div>
|
</h2>
|
||||||
<div>Crédits : <span style={{ color: '#00FF00' }}>{profile.balance} {profile.currency}</span></div>
|
<span style={{ color: '#888' }}>ID Réseau: <span style={{ color: '#E0E0E0' }}>usr_8472_alpha</span></span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div style={{ textAlign: 'right' }}>
|
||||||
|
<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>
|
</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 style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<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>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ZONE DES SERVICES ACTIFS */}
|
{/* MODULE 2 : CLOUD (NEXTCLOUD) */}
|
||||||
{!loading && !error && (
|
<div style={cardStyle}>
|
||||||
<>
|
<div style={statusBadgeStyle}>ACTIF</div>
|
||||||
<h3 style={{ color: '#FFF', marginBottom: '20px' }}>INFRASTRUCTURE ACTIVE</h3>
|
<h3 style={{ color: '#FFF', marginTop: 0, fontSize: '1.2rem' }}>// Sanctuaire Cloud</h3>
|
||||||
|
<p style={{ color: '#888', fontSize: '0.85rem', lineHeight: '1.5' }}>
|
||||||
{orders.length === 0 ? (
|
Stockage chiffré Nextcloud (cloud.gise.be). Synchronisation des terminaux et partage sécurisé.
|
||||||
<div style={{ padding: '30px', textAlign: 'center', border: '1px dashed #333', color: '#888' }}>
|
</p>
|
||||||
Aucun service actif détecté. <br/><br/>
|
<div style={{ margin: '20px 0', fontSize: '0.85rem', color: '#A0A0A0' }}>
|
||||||
<button style={{ backgroundColor: '#00E5FF', color: '#000', border: 'none', padding: '10px 20px', cursor: 'pointer', fontFamily: 'monospace', fontWeight: 'bold' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
+ DÉPLOYER UN NOUVEAU SERVICE
|
<span>Espace Alloué:</span> <span style={{ color: '#FFF' }}>50 Go</span>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '20px' }}>
|
<span>Espace Utilisé:</span> <span style={{ color: '#FFF' }}>1.2 Go</span>
|
||||||
{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>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</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' }}>>_ 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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ export default function Home() {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p style={{
|
<p style={{
|
||||||
color: '#A0A0A0',
|
color: '#A0A0A0',
|
||||||
fontSize: '1.2rem',
|
fontSize: '1.2rem',
|
||||||
maxWidth: '600px',
|
maxWidth: '600px',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
|
|||||||
+56
-29
@@ -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,80 +10,107 @@ 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.
|
||||||
// Si on arrive ici, le login est un succès !
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
// On redirige vers l'espace client sécurisé
|
|
||||||
navigate('/dashboard');
|
// --- 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) {
|
} 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}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
required
|
required
|
||||||
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}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
required
|
required
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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 ></span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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.");
|
||||||
@@ -36,9 +51,9 @@ export default function Register() {
|
|||||||
try {
|
try {
|
||||||
// Envoi de la requête groupée à notre orchestrateur PHP backend
|
// Envoi de la requête groupée à notre orchestrateur PHP backend
|
||||||
await registerUnifiedClient(email, username, password, firstName, lastName);
|
await registerUnifiedClient(email, username, password, firstName, lastName);
|
||||||
|
|
||||||
alert("[ PROVISIONNEMENT RÉUSSI ]\nVos comptes FOSSBilling, HestiaCP et Nextcloud ont été initialisés.\nVous pouvez maintenant vous connecter.");
|
alert("[ PROVISIONNEMENT RÉUSSI ]\nVos comptes FOSSBilling, HestiaCP et Nextcloud ont été initialisés.\nVous pouvez maintenant vous connecter.");
|
||||||
|
|
||||||
// Redirection automatique vers la page de login après succès
|
// Redirection automatique vers la page de login après succès
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -50,8 +65,8 @@ export default function Register() {
|
|||||||
|
|
||||||
// Réutilisation de ton Design System "Bunker"
|
// Réutilisation de ton Design System "Bunker"
|
||||||
const inputStyle = {
|
const inputStyle = {
|
||||||
width: '100%', padding: '10px', marginBottom: '15px',
|
width: '100%', padding: '10px', marginBottom: '15px',
|
||||||
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'
|
boxSizing: 'border-box'
|
||||||
};
|
};
|
||||||
@@ -65,8 +80,8 @@ export default function Register() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '5vh', padding: '0 20px' }}>
|
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '5vh', padding: '0 20px' }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '100%', maxWidth: '500px', backgroundColor: '#242424',
|
width: '100%', maxWidth: '500px', 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: '5px', fontSize: '1.5rem' }}>
|
<h2 style={{ color: '#FFF', textTransform: 'uppercase', marginBottom: '5px', fontSize: '1.5rem' }}>
|
||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user