From bf362106704838d89eb0ffe659117134444562a1 Mon Sep 17 00:00:00 2001 From: maximus Date: Thu, 4 Jun 2026 13:58:32 +0200 Subject: [PATCH] add dashboard, home, login, layout, api --- api-keys.txt | 6 ++ package-lock.json | 60 ++++++++++++- package.json | 3 +- src/App.jsx | 137 +++++------------------------ src/index.css | 112 +----------------------- src/layouts/PublicLayout.jsx | 19 +++++ src/pages/app/Dashboard.jsx | 161 +++++++++++++++++++++++++++++++++++ src/pages/public/Home.jsx | 43 ++++++++++ src/pages/public/Login.jsx | 90 ++++++++++++++++++++ src/services/api.js | 31 +++++++ structure.txt | 53 ++++++++++++ vite.config.js | 13 ++- 12 files changed, 497 insertions(+), 231 deletions(-) create mode 100644 api-keys.txt create mode 100644 src/layouts/PublicLayout.jsx create mode 100644 src/pages/app/Dashboard.jsx create mode 100644 src/pages/public/Home.jsx create mode 100644 src/pages/public/Login.jsx create mode 100644 src/services/api.js create mode 100644 structure.txt diff --git a/api-keys.txt b/api-keys.txt new file mode 100644 index 0000000..2b76093 --- /dev/null +++ b/api-keys.txt @@ -0,0 +1,6 @@ +FossBilling : +fpBTefVk568S4VP36feE6i7XKcqNmvi4 + +Hestiacp : +id : EpIQTIAJVlYGRerVfiNo +secret : PMzs_nKYFelMfa3T9Vu9NgNZDtDFenxjeFCo-7Eq \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ae7259e..da7c763 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "react-router-dom": "^7.16.0" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -1050,6 +1051,19 @@ "dev": true, "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": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2111,6 +2125,44 @@ "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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", @@ -2161,6 +2213,12 @@ "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 40fb9e7..89b8b15 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "react-router-dom": "^7.16.0" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/src/App.jsx b/src/App.jsx index 4f03aa1..64f6f0e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,122 +1,27 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from './assets/vite.svg' -import heroImg from './assets/hero.png' -import './App.css' +// 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'; function App() { - const [count, setCount] = useState(0) - return ( - <> -
-
- - React logo - Vite logo -
-
-

Get started

-

- Edit src/App.jsx and save to test HMR -

-
- -
+ + + {/* === ROUTES PUBLIQUES (Utilisent le PublicLayout) === */} + }> + } /> + } /> + {/* Tu pourras ajouter /offres, /register ici plus tard */} + -
- -
-
- -

Documentation

-

Your questions, answered

- -
-
- -

Connect with us

-

Join the Vite community

- -
-
- -
-
- - ) + {/* === 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 */} + } /> +
+
+ ); } -export default App +export default App; \ No newline at end of file diff --git a/src/index.css b/src/index.css index 2c84af0..827599b 100644 --- a/src/index.css +++ b/src/index.css @@ -1,111 +1 @@ -:root { - --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); -} +body { margin: 0; } \ No newline at end of file diff --git a/src/layouts/PublicLayout.jsx b/src/layouts/PublicLayout.jsx new file mode 100644 index 0000000..69c3f7d --- /dev/null +++ b/src/layouts/PublicLayout.jsx @@ -0,0 +1,19 @@ +// src/layouts/PublicLayout.jsx +import { Outlet, Link } from 'react-router-dom'; + +export default function PublicLayout() { + return ( +
+ + + {/* C'est ici que les pages (Accueil, Login, etc.) vont s'afficher */} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/app/Dashboard.jsx b/src/pages/app/Dashboard.jsx new file mode 100644 index 0000000..82aaa35 --- /dev/null +++ b/src/pages/app/Dashboard.jsx @@ -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 ( +
+ + {/* HEADER DU DASHBOARD */} +
+

CENTRE DE CONTRÔLE GISE

+ {profile && ( +
+
Opérateur : {profile.email}
+
Crédits : {profile.balance} {profile.currency}
+
+ )} +
+ + {loading &&

[ Synchronisation des données en cours... ]

} + + {error && ( +
+ [ ERREUR ] : {error} +
+ )} + + {/* ZONE DES SERVICES ACTIFS */} + {!loading && !error && ( + <> +

INFRASTRUCTURE ACTIVE

+ + {orders.length === 0 ? ( +
+ Aucun service actif détecté.

+ +
+ ) : ( +
+ {orders.map((order) => ( +
+
+ {order.title} + + {order.status} + +
+
Renouvellement : {order.expires_at || 'N/A'}
+
Montant : {order.total} {order.currency}
+ + +
+ ))} +
+ )} + + )} +
+ ); +} \ No newline at end of file diff --git a/src/pages/public/Home.jsx b/src/pages/public/Home.jsx new file mode 100644 index 0000000..cbd16cc --- /dev/null +++ b/src/pages/public/Home.jsx @@ -0,0 +1,43 @@ +export default function Home() { + return ( +
+

+ Bienvenue dans l'infrastructure GISE +

+ +

+ Hébergement web, instances VPS et Stockage Cloud haute sécurité. +
Propulsé par une architecture bare-metal locale. +

+ + {/* Un petit bouton d'action pour la suite */} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/public/Login.jsx b/src/pages/public/Login.jsx new file mode 100644 index 0000000..244605f --- /dev/null +++ b/src/pages/public/Login.jsx @@ -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 ( +
+
+

+ Authentification +

+ + {/* Zone d'erreur qui s'affiche si le mot de passe est faux */} + {error && ( +
+ [ ERREUR ] : {error} +
+ )} + +
+ + setEmail(e.target.value)} + style={inputStyle} + required + placeholder="admin@gise.be" + /> + + + setPassword(e.target.value)} + style={inputStyle} + required + placeholder="••••••••" + /> + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 0000000..27ed264 --- /dev/null +++ b/src/services/api.js @@ -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 }); \ No newline at end of file diff --git a/structure.txt b/structure.txt new file mode 100644 index 0000000..6e4ce85 --- /dev/null +++ b/structure.txt @@ -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/). \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 8b0f57b..9e7ae9b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,16 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -// https://vite.dev/config/ export default defineConfig({ 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 + } + } + } +}) \ No newline at end of file