add dashboard, home, login, layout, api

This commit is contained in:
2026-06-04 13:58:32 +02:00
parent fb2a7485bb
commit bf36210670
12 changed files with 497 additions and 231 deletions
+6
View File
@@ -0,0 +1,6 @@
FossBilling :
fpBTefVk568S4VP36feE6i7XKcqNmvi4
Hestiacp :
id : EpIQTIAJVlYGRerVfiNo
secret : PMzs_nKYFelMfa3T9Vu9NgNZDtDFenxjeFCo-7Eq
+59 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+19
View File
@@ -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>
);
}
+161
View File
@@ -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>
);
}
+43
View File
@@ -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>
);
}
+90
View File
@@ -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>
);
}
+31
View File
@@ -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 });
+53
View File
@@ -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/).
+11 -2
View File
@@ -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
}
}
}
})