add dashboard, home, login, layout, api
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
FossBilling :
|
||||||
|
fpBTefVk568S4VP36feE6i7XKcqNmvi4
|
||||||
|
|
||||||
|
Hestiacp :
|
||||||
|
id : EpIQTIAJVlYGRerVfiNo
|
||||||
|
secret : PMzs_nKYFelMfa3T9Vu9NgNZDtDFenxjeFCo-7Eq
|
||||||
Generated
+59
-1
@@ -9,7 +9,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6"
|
"react-dom": "^19.2.6",
|
||||||
|
"react-router-dom": "^7.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -1050,6 +1051,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -2111,6 +2125,44 @@
|
|||||||
"react": "^19.2.7"
|
"react": "^19.2.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.16.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||||
@@ -2161,6 +2213,12 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|||||||
+2
-1
@@ -11,7 +11,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6"
|
"react-dom": "^19.2.6",
|
||||||
|
"react-router-dom": "^7.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|||||||
+21
-116
@@ -1,122 +1,27 @@
|
|||||||
import { useState } from 'react'
|
// src/App.jsx
|
||||||
import reactLogo from './assets/react.svg'
|
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||||
import viteLogo from './assets/vite.svg'
|
import PublicLayout from './layouts/PublicLayout';
|
||||||
import heroImg from './assets/hero.png'
|
import Home from './pages/public/Home';
|
||||||
import './App.css'
|
import Login from './pages/public/Login';
|
||||||
|
import Dashboard from './pages/app/Dashboard';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<BrowserRouter>
|
||||||
<section id="center">
|
<Routes>
|
||||||
<div className="hero">
|
{/* === ROUTES PUBLIQUES (Utilisent le PublicLayout) === */}
|
||||||
<img src={heroImg} className="base" width="170" height="179" alt="" />
|
<Route element={<PublicLayout />}>
|
||||||
<img src={reactLogo} className="framework" alt="React logo" />
|
<Route path="/" element={<Home />} />
|
||||||
<img src={viteLogo} className="vite" alt="Vite logo" />
|
<Route path="/login" element={<Login />} />
|
||||||
</div>
|
{/* Tu pourras ajouter /offres, /register ici plus tard */}
|
||||||
<div>
|
</Route>
|
||||||
<h1>Get started</h1>
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.jsx</code> and save to test <code>HMR</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="counter"
|
|
||||||
onClick={() => setCount((count) => count + 1)}
|
|
||||||
>
|
|
||||||
Count is {count}
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="ticks"></div>
|
{/* === 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 */}
|
||||||
<section id="next-steps">
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<div id="docs">
|
</Routes>
|
||||||
<svg className="icon" role="presentation" aria-hidden="true">
|
</BrowserRouter>
|
||||||
<use href="/icons.svg#documentation-icon"></use>
|
);
|
||||||
</svg>
|
|
||||||
<h2>Documentation</h2>
|
|
||||||
<p>Your questions, answered</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://vite.dev/" target="_blank">
|
|
||||||
<img className="logo" src={viteLogo} alt="" />
|
|
||||||
Explore Vite
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://react.dev/" target="_blank">
|
|
||||||
<img className="button-icon" src={reactLogo} alt="" />
|
|
||||||
Learn more
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div id="social">
|
|
||||||
<svg className="icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#social-icon"></use>
|
|
||||||
</svg>
|
|
||||||
<h2>Connect with us</h2>
|
|
||||||
<p>Join the Vite community</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://github.com/vitejs/vite" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#github-icon"></use>
|
|
||||||
</svg>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://chat.vite.dev/" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#discord-icon"></use>
|
|
||||||
</svg>
|
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://x.com/vite_js" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#x-icon"></use>
|
|
||||||
</svg>
|
|
||||||
X.com
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://bsky.app/profile/vite.dev" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#bluesky-icon"></use>
|
|
||||||
</svg>
|
|
||||||
Bluesky
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="ticks"></div>
|
|
||||||
<section id="spacer"></section>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
+1
-111
@@ -1,111 +1 @@
|
|||||||
:root {
|
body { margin: 0; }
|
||||||
--text: #6b6375;
|
|
||||||
--text-h: #08060d;
|
|
||||||
--bg: #fff;
|
|
||||||
--border: #e5e4e7;
|
|
||||||
--code-bg: #f4f3ec;
|
|
||||||
--accent: #aa3bff;
|
|
||||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
|
||||||
--accent-border: rgba(170, 59, 255, 0.5);
|
|
||||||
--social-bg: rgba(244, 243, 236, 0.5);
|
|
||||||
--shadow:
|
|
||||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
|
||||||
|
|
||||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
--mono: ui-monospace, Consolas, monospace;
|
|
||||||
|
|
||||||
font: 18px/145% var(--sans);
|
|
||||||
letter-spacing: 0.18px;
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: var(--text);
|
|
||||||
background: var(--bg);
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--text: #9ca3af;
|
|
||||||
--text-h: #f3f4f6;
|
|
||||||
--bg: #16171d;
|
|
||||||
--border: #2e303a;
|
|
||||||
--code-bg: #1f2028;
|
|
||||||
--accent: #c084fc;
|
|
||||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
|
||||||
--accent-border: rgba(192, 132, 252, 0.5);
|
|
||||||
--social-bg: rgba(47, 48, 58, 0.5);
|
|
||||||
--shadow:
|
|
||||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#social .button-icon {
|
|
||||||
filter: invert(1) brightness(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
|
||||||
width: 1126px;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
border-inline: 1px solid var(--border);
|
|
||||||
min-height: 100svh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2 {
|
|
||||||
font-family: var(--heading);
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-h);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 56px;
|
|
||||||
letter-spacing: -1.68px;
|
|
||||||
margin: 32px 0;
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 36px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 118%;
|
|
||||||
letter-spacing: -0.24px;
|
|
||||||
margin: 0 0 8px;
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
code,
|
|
||||||
.counter {
|
|
||||||
font-family: var(--mono);
|
|
||||||
display: inline-flex;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--text-h);
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 135%;
|
|
||||||
padding: 4px 8px;
|
|
||||||
background: var(--code-bg);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// src/layouts/PublicLayout.jsx
|
||||||
|
import { Outlet, Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
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' }}>
|
||||||
|
<Link to="/" style={{ color: '#00E5FF', textDecoration: 'none' }}>[ GISE_BUNKER ]</Link>
|
||||||
|
<Link to="/offres" style={{ color: '#E0E0E0', textDecoration: 'none' }}>Catalogue</Link>
|
||||||
|
<Link to="/login" style={{ color: '#E0E0E0', textDecoration: 'none', marginLeft: 'auto' }}>Connexion</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* C'est ici que les pages (Accueil, Login, etc.) vont s'afficher */}
|
||||||
|
<main style={{ padding: '20px' }}>
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { getClientProfile, getClientOrders, getOrderService } from '../../services/api';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDashboardData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cardStyle = {
|
||||||
|
backgroundColor: '#1A1A1A',
|
||||||
|
border: '1px solid #333',
|
||||||
|
padding: '20px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '10px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleManageInstance = async (orderId) => {
|
||||||
|
try {
|
||||||
|
console.log(`[SYS] Demande d'accès au service #${orderId}...`);
|
||||||
|
const serviceData = await getOrderService(orderId);
|
||||||
|
|
||||||
|
if (serviceData && serviceData.server && serviceData.server.login_url) {
|
||||||
|
window.open(serviceData.server.login_url, '_blank');
|
||||||
|
}
|
||||||
|
else if (serviceData && serviceData.server) {
|
||||||
|
// 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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '40px', color: '#E0E0E0', fontFamily: 'monospace', backgroundColor: '#121212', minHeight: '100vh' }}>
|
||||||
|
|
||||||
|
{/* 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 && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && <p style={{ color: '#888' }}>[ Synchronisation des données en cours... ]</p>}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{ backgroundColor: 'rgba(255, 68, 68, 0.1)', color: '#FF4444', padding: '15px', border: '1px solid #FF4444', marginBottom: '20px' }}>
|
||||||
|
[ ERREUR ] : {error}
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', marginTop: '10vh' }}>
|
||||||
|
<h1 style={{
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: '2.5rem',
|
||||||
|
letterSpacing: '2px',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
marginBottom: '20px'
|
||||||
|
}}>
|
||||||
|
Bienvenue dans l'infrastructure <span style={{ color: '#00E5FF' }}>GISE</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p style={{
|
||||||
|
color: '#A0A0A0',
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
maxWidth: '600px',
|
||||||
|
margin: '0 auto',
|
||||||
|
lineHeight: '1.6'
|
||||||
|
}}>
|
||||||
|
Hébergement web, instances VPS et Stockage Cloud haute sécurité.
|
||||||
|
<br />Propulsé par une architecture bare-metal locale.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Un petit bouton d'action pour la suite */}
|
||||||
|
<div style={{ marginTop: '40px' }}>
|
||||||
|
<button style={{
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: '#00E5FF',
|
||||||
|
border: '1px solid #00E5FF',
|
||||||
|
padding: '12px 24px',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '1px'
|
||||||
|
}}>
|
||||||
|
Démarrer le déploiement
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { loginClient } from '../../services/api';
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLogin = async (e) => {
|
||||||
|
e.preventDefault(); // Empêche la page de se rafraîchir
|
||||||
|
setError(null);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// On envoie la requête à FOSSBilling
|
||||||
|
await loginClient(email, password);
|
||||||
|
|
||||||
|
// Si on arrive ici, le login est un succès !
|
||||||
|
// On redirige vers l'espace client sécurisé
|
||||||
|
navigate('/dashboard');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || "Accès refusé. Vérifiez vos identifiants.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Styles CSS en variables pour garder le code lisible
|
||||||
|
const inputStyle = {
|
||||||
|
width: '100%', padding: '12px', marginBottom: '15px',
|
||||||
|
backgroundColor: '#1A1A1A', color: '#00E5FF',
|
||||||
|
border: '1px solid #333', fontFamily: 'monospace', outline: 'none'
|
||||||
|
};
|
||||||
|
|
||||||
|
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'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '10vh' }}>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleLogin}>
|
||||||
|
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>IDENTIFIANT (EMAIL)</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
style={inputStyle}
|
||||||
|
required
|
||||||
|
placeholder="admin@gise.be"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>CLÉ D'ACCÈS (MOT DE PASSE)</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
style={inputStyle}
|
||||||
|
required
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button type="submit" style={buttonStyle} disabled={loading}>
|
||||||
|
{loading ? 'VÉRIFICATION...' : 'INITIALISER LA CONNEXION'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// src/services/api.js
|
||||||
|
|
||||||
|
const apiCall = async (url, body = {}) => {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include', // Garde bien ça pour envoyer le cookie de session
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.error) throw new Error(data.error.message);
|
||||||
|
return data.result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginClient = (email, password) =>
|
||||||
|
apiCall('/api/guest/client/login', { email, password });
|
||||||
|
|
||||||
|
export const getClientProfile = () =>
|
||||||
|
apiCall('/api/client/profile/get');
|
||||||
|
|
||||||
|
// Récupère la liste des services/commandes du client
|
||||||
|
export const getClientOrders = () =>
|
||||||
|
apiCall('/api/client/order/get_list');
|
||||||
|
|
||||||
|
// Récupère les détails techniques du service rattaché à une commande
|
||||||
|
export const getOrderService = (order_id) =>
|
||||||
|
apiCall('/api/client/order/service', { id: order_id });
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
src/
|
||||||
|
├── assets/ # Images, logo GISE
|
||||||
|
├── components/ # Les petits morceaux réutilisables (Boutons, Cartes, Navbar, Footer)
|
||||||
|
├── layouts/ # Les "moules" des pages
|
||||||
|
│ ├── PublicLayout.jsx # Moule avec Header/Footer standard
|
||||||
|
│ └── AppLayout.jsx # Moule avec Barre latérale (Sidebar) pour l'espace client
|
||||||
|
├── pages/ # Les grandes pages
|
||||||
|
│ ├── public/ # Accueil, Login, Register...
|
||||||
|
│ └── app/ # Dashboard, Services, Factures...
|
||||||
|
├── services/ # Le "moteur" : fonctions pour parler à l'API FOSSBilling/Hestia
|
||||||
|
└── App.jsx # Le routeur principal
|
||||||
|
|
||||||
|
|
||||||
|
1. Les Routes Publiques (La Vitrine)
|
||||||
|
Ces pages sont accessibles à tout le monde.
|
||||||
|
|
||||||
|
/ (Accueil) : La page d'atterrissage. Une grande image forte (le Bunker), les 3 piliers (Web, VPS, Cloud), et un gros appel à l'action.
|
||||||
|
|
||||||
|
/offres (Catalogue global) : Une page qui résume tous les produits.
|
||||||
|
|
||||||
|
/offres/web : Détail des offres d'hébergement HestiaCP.
|
||||||
|
|
||||||
|
/offres/vps : Détail des serveurs n8n, etc.
|
||||||
|
|
||||||
|
/offres/cloud : Détail des offres Nextcloud.
|
||||||
|
|
||||||
|
/login (Connexion) : Le formulaire pour entrer dans le Bunker.
|
||||||
|
|
||||||
|
/register (Inscription) : Le formulaire pour créer un compte.
|
||||||
|
|
||||||
|
/password-reset (Mot de passe oublié) : Essentiel.
|
||||||
|
|
||||||
|
2. Les Routes Privées (L'Espace Client protégé)
|
||||||
|
Ces routes nécessitent que l'utilisateur ait un token valide (il est connecté). Si un visiteur non connecté essaie d'y aller, il sera redirigé vers /login.
|
||||||
|
|
||||||
|
/dashboard (Vue d'ensemble) : Le centre de contrôle.
|
||||||
|
|
||||||
|
/services (Inventaire global) : La liste de tout ce que possède le client.
|
||||||
|
|
||||||
|
/services/web : Ses sites hébergés sur HestiaCP (avec le bouton SSO magique).
|
||||||
|
|
||||||
|
/services/vps : Ses serveurs.
|
||||||
|
|
||||||
|
/services/cloud : Son stockage Nextcloud.
|
||||||
|
|
||||||
|
/billing (Facturation) : Historique, factures impayées (relié à FOSSBilling).
|
||||||
|
|
||||||
|
/support (Tickets) : Ouvrir et suivre des demandes d'aide.
|
||||||
|
|
||||||
|
/settings (Profil) : Gérer ses infos perso et mots de passe.
|
||||||
|
|
||||||
|
III. Comment structurer ça dans ton code React ?
|
||||||
|
Pour garder un code propre, tu vas créer une arborescence de dossiers logique dans ton projet Vite (dossier src/).
|
||||||
+10
-1
@@ -1,7 +1,16 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
// Toutes les requêtes qui commencent par /api iront vers ton FOSSBilling
|
||||||
|
'/api': {
|
||||||
|
target: 'https://web.gise.be',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user