This commit is contained in:
+407
-152
@@ -1,45 +1,347 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { getMyServices, getServiceDetails, getHostingServiceDetails, resetHostingPassword, launchSSOGateway } from '../../services/api';
|
import { getMyServices, getServiceDetails, getHostingServiceDetails, resetHostingPassword } from '../../services/api';
|
||||||
import { useVPC } from '../../services/useVPC';
|
import { useVPC } from '../../services/useVPC';
|
||||||
import { Server, Database, Cloud, Globe, Folder, Trash2, ExternalLink, Loader, Plus } from 'lucide-react';
|
import { Server, Database, Cloud, Globe, Folder, Trash2, ExternalLink, Loader, Plus, Play } from 'lucide-react';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// COMPOSANT ISOLÉ : INSTANCE CARD
|
||||||
|
// ============================================================================
|
||||||
|
const InstanceCard = ({ service, vpcs, onAssignVpc, onRemoveVpc, onOpenConsole, isConnecting }) => {
|
||||||
|
const titleLower = (service.title || '').toLowerCase();
|
||||||
|
const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
|
||||||
|
const isCloud = titleLower.includes('cloud') || titleLower.includes('nextcloud');
|
||||||
|
const isDB = titleLower.includes('db') || titleLower.includes('base') || titleLower.includes('sql');
|
||||||
|
|
||||||
|
// Vérification de l'état d'initialisation pour le VPS
|
||||||
|
const hasIP = service.hostingDetails?.ip && service.hostingDetails.ip !== '127.0.0.1' && service.hostingDetails.ip !== '';
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// DESIGN DES BOUTONS PAR TYPE DE SERVICE
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
let buttonText = "CONSOLE D'ADMINISTRATION";
|
||||||
|
let ButtonIcon = ExternalLink;
|
||||||
|
let buttonStyle = "bg-cyan-400/10 hover:bg-cyan-400 text-cyan-400 hover:text-gray-900 border-cyan-400";
|
||||||
|
|
||||||
|
if (isVPS) {
|
||||||
|
if (hasIP) {
|
||||||
|
buttonText = "GÉRER L'INSTANCE";
|
||||||
|
ButtonIcon = ExternalLink;
|
||||||
|
} else {
|
||||||
|
buttonText = "INITIALISATION";
|
||||||
|
ButtonIcon = Play;
|
||||||
|
buttonStyle = "bg-yellow-500/10 text-yellow-500 hover:bg-yellow-500 hover:text-gray-900 border-yellow-500/50";
|
||||||
|
}
|
||||||
|
} else if (isCloud) {
|
||||||
|
buttonText = "ACCÉDER AU CLOUD";
|
||||||
|
ButtonIcon = ExternalLink;
|
||||||
|
buttonStyle = "bg-blue-400/10 hover:bg-blue-400 text-blue-400 hover:text-gray-900 border-blue-400";
|
||||||
|
} else if (isDB) {
|
||||||
|
buttonText = "PHPMYADMIN / CLUSTER";
|
||||||
|
ButtonIcon = Database;
|
||||||
|
buttonStyle = "bg-purple-400/10 hover:bg-purple-400 text-purple-400 hover:text-gray-900 border-purple-400";
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServiceIcon = () => {
|
||||||
|
if (isVPS) return <Server className="w-8 h-8 text-cyan-400" />;
|
||||||
|
if (isCloud) return <Cloud className="w-8 h-8 text-blue-400" />;
|
||||||
|
if (isDB) return <Database className="w-8 h-8 text-purple-400" />;
|
||||||
|
return <Globe className="w-8 h-8 text-emerald-400" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// NETTOYAGE DU TEXTE (Titre à 2 mots & Extraction de domaine)
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 1. Coupe le "pour X" ou "for X" du titre, puis garde les 2 premiers mots
|
||||||
|
const baseTitle = (service.title || '').split(/(?: for | pour )/i)[0].trim();
|
||||||
|
const shortTitle = baseTitle.split(' ').slice(0, 2).join(' ');
|
||||||
|
|
||||||
|
// 2. Extraction du domaine qui se trouve après "pour" ou "for" dans le titre FOSSBilling
|
||||||
|
const titleMatch = (service.title || '').match(/(?: for | pour )(.+)$/i);
|
||||||
|
const domainFromTitle = titleMatch ? titleMatch[1].trim() : null;
|
||||||
|
|
||||||
|
// 3. Choix du domaine à afficher (Priorité à la BDD, sinon on prend celui du titre)
|
||||||
|
let displayDomain = service.hostingDetails?.domain && service.hostingDetails.domain !== '127.0.0.1'
|
||||||
|
? service.hostingDetails.domain
|
||||||
|
: domainFromTitle || service.domain;
|
||||||
|
|
||||||
|
if (!displayDomain || displayDomain === '127.0.0.1') {
|
||||||
|
displayDomain = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-900 border border-gray-800 rounded-xl p-5 flex flex-col justify-between hover:border-cyan-400/30 transition-all shadow-lg">
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-start mb-4">
|
||||||
|
<div className="p-2 bg-black/40 rounded-lg">
|
||||||
|
{getServiceIcon()}
|
||||||
|
</div>
|
||||||
|
{service.status === 'active' ? (
|
||||||
|
<span className="bg-emerald-500/10 text-emerald-400 px-2 py-1 rounded text-xs border border-emerald-500/20 font-bold tracking-widest">
|
||||||
|
{isVPS && !hasIP ? 'AWAITING INIT' : 'ONLINE'}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="bg-orange-500/10 text-orange-400 px-2 py-1 rounded text-xs border border-orange-500/20 animate-pulse">DEPLOYING</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="font-bold text-white text-lg truncate" title={baseTitle}>
|
||||||
|
{shortTitle}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Espace fixe pour le domaine (remplace l'ancien CMD: #id) */}
|
||||||
|
<div className="h-6 mt-1 mb-4 flex items-center">
|
||||||
|
{displayDomain ? (
|
||||||
|
<p className="text-cyan-400 text-xs font-mono truncate" title={displayDomain}>
|
||||||
|
{displayDomain}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-600 text-xs font-mono italic">
|
||||||
|
Non configuré
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-auto space-y-3">
|
||||||
|
<div className="flex items-center justify-between text-sm border-t border-gray-800 pt-3">
|
||||||
|
<span className="text-gray-500 text-xs">Projet VPC:</span>
|
||||||
|
<select
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === "free") onRemoveVpc(service.id);
|
||||||
|
else if (val) onAssignVpc(service.id, val);
|
||||||
|
}}
|
||||||
|
className="bg-black border border-gray-700 text-gray-300 rounded px-2 py-1 outline-none focus:border-cyan-400 text-xs w-[130px]"
|
||||||
|
defaultValue={vpcs.find(v => v.services.includes(service.id))?.id || "free"}
|
||||||
|
>
|
||||||
|
<option value="free">-- Libre --</option>
|
||||||
|
{vpcs.map(vpc => (
|
||||||
|
<option key={vpc.id} value={vpc.id}>{vpc.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onOpenConsole(service)}
|
||||||
|
disabled={isConnecting === service.id || service.status !== 'active'}
|
||||||
|
className={`w-full flex items-center justify-center space-x-2 py-2.5 rounded-lg font-bold text-sm tracking-wide transition-all disabled:opacity-50 border ${buttonStyle}`}
|
||||||
|
>
|
||||||
|
{isConnecting === service.id ? (
|
||||||
|
<><Loader className="w-4 h-4 animate-spin" /><span>CONNEXION...</span></>
|
||||||
|
) : (
|
||||||
|
<><ButtonIcon className="w-4 h-4" /><span>{buttonText}</span></>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MODAL : VPS DEPLOYER
|
||||||
|
// ============================================================================
|
||||||
|
const VpsDeployer = ({ serviceId, onDeploySuccess }) => {
|
||||||
|
const [domain, setDomain] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
|
||||||
|
const handleDeploy = async () => {
|
||||||
|
if (password.length < 8) { alert("Le mot de passe doit faire au moins 8 caractères."); return; }
|
||||||
|
|
||||||
|
setIsDeploying(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://web.gise.be/custom_api/proxmox_create_vps.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({ service_id: serviceId, domain: domain, password: password })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
onDeploySuccess({ ip: data.ip, domain: data.domain, password: password });
|
||||||
|
} else {
|
||||||
|
alert("Erreur de provisionnement : " + (data.message || 'Inconnue'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Erreur de communication avec l'API Proxmox Gateway.");
|
||||||
|
} finally {
|
||||||
|
setIsDeploying(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="bg-yellow-900/20 border border-yellow-600/30 p-4 rounded mb-6 text-yellow-500/80 text-sm">
|
||||||
|
<span className="font-bold text-yellow-500">PHASE D'INITIALISATION REQUISE</span><br/>
|
||||||
|
La facturation est activée. Veuillez définir un mot de passe Root pour lancer la création de l'instance sur l'hyperviseur.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Hostname personnalisé (Optionnel)</label>
|
||||||
|
<input type="text" value={domain} onChange={(e) => setDomain(e.target.value.toLowerCase())} placeholder="Laissez vide pour utiliser l'IP" className="w-full bg-gray-950 border border-gray-800 text-white p-3 rounded font-mono outline-none focus:border-cyan-500 transition-colors" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Mot de passe Root (Requis)</label>
|
||||||
|
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="••••••••" className="w-full bg-gray-950 border border-gray-800 text-white p-3 rounded font-mono outline-none focus:border-cyan-500 transition-colors" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={handleDeploy} disabled={isDeploying} className={`mt-6 w-full px-6 py-3 rounded font-bold font-mono tracking-widest uppercase transition ${isDeploying ? 'bg-gray-800 text-gray-500 cursor-not-allowed' : 'bg-cyan-600 hover:bg-cyan-500 text-white shadow-lg shadow-cyan-500/20'}`}>
|
||||||
|
{isDeploying ? 'Fabrication sur le Métal en cours...' : 'Lancer l\'Instanciation'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MODAL : VPS MANAGER (Modèle PaaS Complet)
|
||||||
|
// ============================================================================
|
||||||
|
const VpsManager = ({ details }) => {
|
||||||
|
const [showTerminal, setShowTerminal] = useState(false);
|
||||||
|
|
||||||
|
// Les URLs de tes outils internes
|
||||||
|
const fileManagerUrl = `https://file.${details.domain}`;
|
||||||
|
const terminalUrl = `https://terminal.${details.domain}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!showTerminal ? (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
{/* BLOC RÉSEAU */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Identifiant Réseau</label>
|
||||||
|
<div className="bg-gray-950 border border-gray-800 rounded p-3 font-mono text-green-400 font-bold flex items-center gap-2">
|
||||||
|
<span className="h-2 w-2 rounded-full bg-green-500 shadow-[0_0_8px_#22c55e]"></span>
|
||||||
|
{details.domain}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BLOC OUTILS MANAGÉS */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Fichiers (FileBrowser)</label>
|
||||||
|
<button
|
||||||
|
onClick={() => window.open(fileManagerUrl, '_blank')}
|
||||||
|
className="w-full bg-gray-950 hover:bg-gray-900 border border-gray-800 hover:border-cyan-500/50 rounded p-3 flex justify-between items-center transition group"
|
||||||
|
>
|
||||||
|
<span className="font-mono text-gray-300 text-sm flex items-center gap-2">
|
||||||
|
<Folder className="w-4 h-4 text-cyan-500 group-hover:text-cyan-400" />
|
||||||
|
Ouvrir l'explorateur
|
||||||
|
</span>
|
||||||
|
<ExternalLink className="w-4 h-4 text-gray-600 group-hover:text-cyan-400" />
|
||||||
|
</button>
|
||||||
|
<p className="text-[10px] text-gray-600 mt-1 uppercase">• Utilisateur: root</p>
|
||||||
|
<p className="text-[10px] text-gray-600 mt-1 uppercase">• Pass: NexusGise2026! (à modifier)</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Console Système</label>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowTerminal(true)}
|
||||||
|
className="w-full bg-cyan-900/20 hover:bg-cyan-900/40 border border-cyan-800/50 hover:border-cyan-500 rounded p-3 flex justify-between items-center transition group"
|
||||||
|
>
|
||||||
|
<span className="font-mono text-cyan-400 text-sm flex items-center gap-2">
|
||||||
|
<span className="font-bold">{">_"}</span>
|
||||||
|
Ouvrir le Terminal
|
||||||
|
</span>
|
||||||
|
<Play className="w-4 h-4 text-cyan-600 group-hover:text-cyan-400" />
|
||||||
|
</button>
|
||||||
|
<p className="text-[10px] text-gray-600 mt-1 uppercase">• Utilisateur: root</p>
|
||||||
|
<p className="text-[10px] text-gray-600 mt-1 uppercase">• Pass: mot de passe d'initialisation</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex gap-3 border-t border-gray-800 pt-6">
|
||||||
|
<button className="flex-1 bg-gray-800 hover:bg-gray-700 text-white px-4 py-3 rounded text-sm font-mono font-bold tracking-widest transition">↻ REBOOT</button>
|
||||||
|
<button className="flex-1 bg-red-900/20 hover:bg-red-900/40 text-red-400 px-4 py-3 rounded text-sm font-mono font-bold tracking-widest border border-red-900/30 transition">■ STOP</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
/* VUE CONSOLE INTÉGRÉE */
|
||||||
|
<div className="animate-in fade-in duration-300">
|
||||||
|
<div className="flex justify-between items-center mb-4 border-b border-gray-800 pb-2">
|
||||||
|
<span className="text-cyan-400 font-mono text-sm tracking-widest flex items-center gap-2">
|
||||||
|
<span className="h-2 w-2 bg-cyan-400 rounded-full animate-pulse"></span>
|
||||||
|
NEXUS TERMINAL ({details.domain})
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => window.open(terminalUrl, '_blank')}
|
||||||
|
className="text-xs bg-gray-800 hover:bg-gray-700 text-cyan-400 px-3 py-1 rounded transition flex items-center gap-1"
|
||||||
|
title="Ouvrir dans un nouvel onglet"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3 h-3" /> Plein écran
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowTerminal(false)}
|
||||||
|
className="text-xs bg-red-900/30 hover:bg-red-900/60 text-white px-3 py-1 rounded transition"
|
||||||
|
>
|
||||||
|
Fermer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Le conteneur iFrame qui charge TTYD (xterm.js) */}
|
||||||
|
<div className="bg-black border border-gray-800 rounded h-[400px] overflow-hidden relative">
|
||||||
|
{/* Message d'aide qui s'affiche brièvement pendant le chargement de l'iframe */}
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center -z-10 text-gray-500 font-mono text-sm">
|
||||||
|
<Loader className="w-8 h-8 animate-spin mb-4 text-cyan-500" />
|
||||||
|
<p>Chargement du moteur xterm.js...</p>
|
||||||
|
<p className="mt-2 text-xs">Identifiant : <span className="text-white">root</span></p>
|
||||||
|
<p className="text-xs">Mot de passe : <span className="text-white">Celui d'initialisation</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
src={terminalUrl}
|
||||||
|
className="w-full h-full border-none z-10"
|
||||||
|
title={`Terminal for ${details.domain}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// COMPOSANT PRINCIPAL : SERVICES
|
||||||
|
// ============================================================================
|
||||||
export default function Services() {
|
export default function Services() {
|
||||||
const [services, setServices] = useState([]);
|
const [services, setServices] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [newVpcName, setNewVpcName] = useState("");
|
const [newVpcName, setNewVpcName] = useState("");
|
||||||
const [isConnecting, setIsConnecting] = useState(null); // Pour le loader du bouton Auto-Login
|
const [isConnecting, setIsConnecting] = useState(null);
|
||||||
const [ssoVault, setSsoVault] = useState(null);
|
const [ssoVault, setSsoVault] = useState(null);
|
||||||
|
const [activeVpsModal, setActiveVpsModal] = useState(null);
|
||||||
|
|
||||||
// Branchement du moteur logique VPC
|
|
||||||
const { vpcs, createVPC, deleteVPC, assignToVPC, removeFromVPC } = useVPC();
|
const { vpcs, createVPC, deleteVPC, assignToVPC, removeFromVPC } = useVPC();
|
||||||
|
|
||||||
// 1. Récupération et Filtrage des Infrastructures
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchServices = async () => {
|
const fetchServices = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getMyServices();
|
const data = await getMyServices();
|
||||||
|
|
||||||
if (data.list && data.list.length > 0) {
|
if (data.list && data.list.length > 0) {
|
||||||
// On récupère les mots de passe de chaque service pour le bouton Auto-Login
|
|
||||||
const detailedServices = await Promise.all(
|
const detailedServices = await Promise.all(
|
||||||
data.list.map(async (order) => {
|
data.list.map(async (order) => {
|
||||||
const details = await getServiceDetails(order.id);
|
let hDetails = null;
|
||||||
return details;
|
if (order.plugin === 'hosting' || (order.title||'').toLowerCase().includes('vps')) {
|
||||||
|
try { hDetails = await getHostingServiceDetails(order.id); } catch(e){}
|
||||||
|
}
|
||||||
|
return { ...order, hostingDetails: hDetails };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// LE FILTRE CHIRURGICAL : On exclut les produits "Domaine" fantômes
|
|
||||||
const filteredServices = detailedServices.filter(s => {
|
const filteredServices = detailedServices.filter(s => {
|
||||||
const type = (s.type || '').toLowerCase();
|
const type = (s.type || '').toLowerCase();
|
||||||
const title = (s.title || '').toLowerCase();
|
const title = (s.title || '').toLowerCase();
|
||||||
const isGhostProduct =
|
// Ajout du mot-clé "enregistrement" au filtre d'exclusion
|
||||||
type === 'domain' ||
|
const isGhostProduct = type === 'domain' || title.includes('domain') || title.includes('domaine') || title.includes('enregistrement');
|
||||||
title.startsWith('domain ') ||
|
|
||||||
title.startsWith('domaine ') ||
|
|
||||||
title.startsWith('enregistrement ');
|
|
||||||
|
|
||||||
// On garde les actifs/en préparation qui ne sont pas des domaines
|
|
||||||
return (s.status === 'active' || s.status === 'pending_setup') && !isGhostProduct;
|
return (s.status === 'active' || s.status === 'pending_setup') && !isGhostProduct;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,102 +356,45 @@ export default function Services() {
|
|||||||
fetchServices();
|
fetchServices();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 2. Moteur de Connexion Furtive (SSO HestiaCP) avec contournement du bloqueur de pop-up
|
const handleOpenConsole = async (service) => {
|
||||||
const handleAutoLogin = async (service) => {
|
const titleLower = (service.title || '').toLowerCase();
|
||||||
|
const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
|
||||||
|
const isCloud = titleLower.includes('cloud') || titleLower.includes('nextcloud');
|
||||||
|
const isDB = titleLower.includes('db') || titleLower.includes('base') || titleLower.includes('sql');
|
||||||
|
|
||||||
setIsConnecting(service.id);
|
setIsConnecting(service.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Récupération de l'utilisateur
|
if (isVPS) {
|
||||||
const hostingDetails = await getHostingServiceDetails(service.id);
|
const freshDetails = await getHostingServiceDetails(service.id);
|
||||||
const username = hostingDetails.username;
|
setActiveVpsModal({ ...service, hostingDetails: freshDetails });
|
||||||
if (!username) throw new Error("Infrastructure non synchronisée avec le métal.");
|
} else if (isCloud) {
|
||||||
|
// Redirection Cloud
|
||||||
|
const titleMatch = (service.title || '').match(/(?: for | pour )(.+)$/i);
|
||||||
|
const domainUrl = titleMatch ? titleMatch[1].trim() : service.domain;
|
||||||
|
if(domainUrl) window.open(`https://${domainUrl}`, '_blank');
|
||||||
|
} else if (isDB) {
|
||||||
|
// Redirection Base de données
|
||||||
|
window.open('https://pma.gise.be/', '_blank');
|
||||||
|
} else {
|
||||||
|
// Redirection Web / HestiaCP
|
||||||
|
const hostingDetails = await getHostingServiceDetails(service.id);
|
||||||
|
const username = hostingDetails.username;
|
||||||
|
if (!username) throw new Error("Infrastructure non synchronisée avec le métal.");
|
||||||
|
|
||||||
// 2. Ghost Reset (Génération du mot de passe jetable)
|
const secureHash = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-4).toUpperCase();
|
||||||
const secureHash = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-4).toUpperCase();
|
const rollingPassword = `Nx${secureHash}`;
|
||||||
const rollingPassword = `Nx${secureHash}`;
|
await resetHostingPassword(service.id, rollingPassword);
|
||||||
|
|
||||||
console.log("Ghost Reset généré pour", username);
|
|
||||||
await resetHostingPassword(service.id, rollingPassword);
|
|
||||||
|
|
||||||
// 3. On affiche le Coffre-Fort à l'utilisateur
|
|
||||||
setSsoVault({
|
|
||||||
username: username,
|
|
||||||
password: rollingPassword,
|
|
||||||
url: 'https://panel.gise.be/login/'
|
|
||||||
});
|
|
||||||
|
|
||||||
|
setSsoVault({ username: username, password: rollingPassword, url: 'https://panel.gise.be/login/' });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Échec du protocole d'accès : " + err.message);
|
alert("Échec du protocole : " + err.message);
|
||||||
} finally {
|
} finally {
|
||||||
setIsConnecting(null);
|
setIsConnecting(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Utilitaires UI
|
|
||||||
const getServiceIcon = (title) => {
|
|
||||||
const t = (title || '').toLowerCase();
|
|
||||||
if (t.includes('vps') || t.includes('serveur')) return <Server className="w-8 h-8 text-cyan-400" />;
|
|
||||||
if (t.includes('cloud') || t.includes('nextcloud')) return <Cloud className="w-8 h-8 text-blue-400" />;
|
|
||||||
if (t.includes('db') || t.includes('base') || t.includes('sql')) return <Database className="w-8 h-8 text-purple-400" />;
|
|
||||||
return <Globe className="w-8 h-8 text-emerald-400" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Composant Interne : La Carte d'Instance
|
|
||||||
const InstanceCard = ({ service }) => (
|
|
||||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-5 flex flex-col justify-between hover:border-cyan-400/30 transition-all">
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-start mb-4">
|
|
||||||
<div className="p-2 bg-black/40 rounded-lg">
|
|
||||||
{getServiceIcon(service.title)}
|
|
||||||
</div>
|
|
||||||
{service.status === 'active' ? (
|
|
||||||
<span className="bg-emerald-500/10 text-emerald-400 px-2 py-1 rounded text-xs border border-emerald-500/20">ONLINE</span>
|
|
||||||
) : (
|
|
||||||
<span className="bg-orange-500/10 text-orange-400 px-2 py-1 rounded text-xs border border-orange-500/20 animate-pulse">DEPLOYING</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 className="font-bold text-white text-lg truncate" title={service.title}>{service.title}</h3>
|
|
||||||
<p className="text-cyan-400 text-xs font-mono mt-1 mb-4">{service.domain || `ID: #${service.id}`}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-auto space-y-3">
|
|
||||||
{/* Sélecteur VPC */}
|
|
||||||
<div className="flex items-center justify-between text-sm border-t border-gray-800 pt-3">
|
|
||||||
<span className="text-gray-500 text-xs">Projet:</span>
|
|
||||||
<select
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
if (val === "free") removeFromVPC(service.id);
|
|
||||||
else if (val) assignToVPC(service.id, val);
|
|
||||||
}}
|
|
||||||
className="bg-black border border-gray-700 text-gray-300 rounded px-2 py-1 outline-none focus:border-cyan-400 text-xs w-[140px]"
|
|
||||||
defaultValue={vpcs.find(v => v.services.includes(service.id))?.id || "free"}
|
|
||||||
>
|
|
||||||
<option value="free">-- Libre --</option>
|
|
||||||
{vpcs.map(vpc => (
|
|
||||||
<option key={vpc.id} value={vpc.id}>{vpc.name}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bouton d'accès Auto-Login */}
|
|
||||||
<button
|
|
||||||
onClick={() => handleAutoLogin(service)}
|
|
||||||
disabled={isConnecting === service.id || service.status !== 'active'}
|
|
||||||
className="w-full flex items-center justify-center space-x-2 bg-cyan-400/10 hover:bg-cyan-400 text-cyan-400 hover:text-gray-900 border border-cyan-400 py-2.5 rounded-lg font-bold text-sm tracking-wide transition-all disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isConnecting === service.id ? (
|
|
||||||
<><Loader className="w-4 h-4 animate-spin" /><span>CONNEXION...</span></>
|
|
||||||
) : (
|
|
||||||
<><ExternalLink className="w-4 h-4" /><span>CONSOLE D'ADMINISTRATION</span></>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Tri des instances (Libres vs Assignées)
|
|
||||||
const assignedServiceIds = vpcs.flatMap(vpc => vpc.services);
|
const assignedServiceIds = vpcs.flatMap(vpc => vpc.services);
|
||||||
const freeServices = services.filter(s => !assignedServiceIds.includes(s.id));
|
const freeServices = services.filter(s => !assignedServiceIds.includes(s.id));
|
||||||
|
|
||||||
@@ -157,8 +402,6 @@ export default function Services() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto mt-8 p-6">
|
<div className="max-w-7xl mx-auto mt-8 p-6">
|
||||||
|
|
||||||
{/* EN-TÊTE ET CRÉATION VPC */}
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-10 gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-10 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-black text-white tracking-wider">INVENTAIRE RÉSEAU</h1>
|
<h1 className="text-3xl font-black text-white tracking-wider">INVENTAIRE RÉSEAU</h1>
|
||||||
@@ -166,18 +409,8 @@ export default function Services() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex space-x-2 bg-gray-900 p-2 rounded-xl border border-gray-800">
|
<div className="flex space-x-2 bg-gray-900 p-2 rounded-xl border border-gray-800">
|
||||||
<input
|
<input type="text" placeholder="Nom du nouveau VPC..." value={newVpcName} onChange={(e) => setNewVpcName(e.target.value)} className="bg-black border border-gray-800 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:border-cyan-400 w-64" />
|
||||||
type="text"
|
<button onClick={() => { createVPC(newVpcName); setNewVpcName(""); }} disabled={!newVpcName.trim()} className="bg-cyan-400 text-gray-900 px-4 py-2 rounded-lg font-bold text-sm disabled:opacity-50 hover:bg-cyan-300 flex items-center">
|
||||||
placeholder="Nom du nouveau VPC..."
|
|
||||||
value={newVpcName}
|
|
||||||
onChange={(e) => setNewVpcName(e.target.value)}
|
|
||||||
className="bg-black border border-gray-800 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:border-cyan-400 w-64"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => { createVPC(newVpcName); setNewVpcName(""); }}
|
|
||||||
disabled={!newVpcName.trim()}
|
|
||||||
className="bg-cyan-400 text-gray-900 px-4 py-2 rounded-lg font-bold text-sm disabled:opacity-50 hover:bg-cyan-300 flex items-center"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4 mr-1" /> CRÉER GROUPE
|
<Plus className="w-4 h-4 mr-1" /> CRÉER GROUPE
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,11 +418,9 @@ export default function Services() {
|
|||||||
|
|
||||||
{error && <div className="text-red-400 bg-red-400/10 p-4 rounded-xl border border-red-500/20 mb-6">{error}</div>}
|
{error && <div className="text-red-400 bg-red-400/10 p-4 rounded-xl border border-red-500/20 mb-6">{error}</div>}
|
||||||
|
|
||||||
{/* LES VPC (Groupes de projets) */}
|
|
||||||
<div className="space-y-8 mb-12">
|
<div className="space-y-8 mb-12">
|
||||||
{vpcs.map(vpc => {
|
{vpcs.map(vpc => {
|
||||||
const vpcServices = services.filter(s => vpc.services.includes(s.id));
|
const vpcServices = services.filter(s => vpc.services.includes(s.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={vpc.id} className="bg-gray-900/40 border border-gray-800 rounded-2xl p-6">
|
<div key={vpc.id} className="bg-gray-900/40 border border-gray-800 rounded-2xl p-6">
|
||||||
<div className="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
|
<div className="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
|
||||||
@@ -202,14 +433,23 @@ export default function Services() {
|
|||||||
<Trash2 className="w-4 h-4 mr-1" /> Démanteler VPC
|
<Trash2 className="w-4 h-4 mr-1" /> Démanteler VPC
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
{vpcServices.length === 0 ? (
|
{vpcServices.length === 0 ? (
|
||||||
<div className="col-span-full text-gray-600 text-sm border border-dashed border-gray-800 rounded-xl p-6 text-center font-mono">
|
<div className="col-span-full text-gray-600 text-sm border border-dashed border-gray-800 rounded-xl p-6 text-center font-mono">
|
||||||
Réseau virtuel vide. Assigner des instances depuis le pool libre.
|
Réseau virtuel vide. Assigner des instances depuis le pool libre.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
vpcServices.map(service => <InstanceCard key={service.id} service={service} />)
|
vpcServices.map(service => (
|
||||||
|
<InstanceCard
|
||||||
|
key={service.id}
|
||||||
|
service={service}
|
||||||
|
vpcs={vpcs}
|
||||||
|
onAssignVpc={assignToVPC}
|
||||||
|
onRemoveVpc={removeFromVPC}
|
||||||
|
onOpenConsole={handleOpenConsole}
|
||||||
|
isConnecting={isConnecting}
|
||||||
|
/>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,74 +457,89 @@ export default function Services() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* LE POOL LIBRE (Instances non groupées) */}
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-bold text-gray-500 tracking-wider mb-6 flex items-center">
|
<h2 className="text-lg font-bold text-gray-500 tracking-wider mb-6 flex items-center">
|
||||||
<Server className="w-5 h-5 mr-2" /> POOL D'INSTANCES LIBRES
|
<Server className="w-5 h-5 mr-2" /> POOL D'INSTANCES LIBRES
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
{freeServices.length === 0 ? (
|
{freeServices.length === 0 ? (
|
||||||
<div className="col-span-full text-gray-600 text-sm border border-gray-900 bg-gray-900/20 rounded-xl p-6 text-center font-mono">
|
<div className="col-span-full text-gray-600 text-sm border border-gray-900 bg-gray-900/20 rounded-xl p-6 text-center font-mono">
|
||||||
Aucune instance libre. Toutes vos accréditations sont assignées à des VPC.
|
Aucune instance libre. Toutes vos accréditations sont assignées à des VPC.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
freeServices.map(service => <InstanceCard key={service.id} service={service} />)
|
freeServices.map(service => (
|
||||||
|
<InstanceCard
|
||||||
|
key={service.id}
|
||||||
|
service={service}
|
||||||
|
vpcs={vpcs}
|
||||||
|
onAssignVpc={assignToVPC}
|
||||||
|
onRemoveVpc={removeFromVPC}
|
||||||
|
onOpenConsole={handleOpenConsole}
|
||||||
|
isConnecting={isConnecting}
|
||||||
|
/>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* MODAL DU COFFRE-FORT ÉPHÉMÈRE */}
|
|
||||||
|
{/* MODAL 1 : HESTIACP SSO VAULT */}
|
||||||
{ssoVault && (
|
{ssoVault && (
|
||||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||||
<div className="bg-gray-900 border border-cyan-500/50 rounded-lg shadow-2xl shadow-cyan-500/20 max-w-md w-full p-6 text-gray-200">
|
<div className="bg-gray-900 border border-cyan-500/50 rounded-lg shadow-2xl shadow-cyan-500/20 max-w-md w-full p-6 text-gray-200">
|
||||||
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h3 className="text-xl font-bold text-cyan-400 font-mono tracking-wider">ACCÈS AUTORISÉ</h3>
|
<h3 className="text-xl font-bold text-cyan-400 font-mono tracking-wider">ACCÈS AUTORISÉ</h3>
|
||||||
<button onClick={() => setSsoVault(null)} className="text-gray-400 hover:text-white transition">
|
<button onClick={() => setSsoVault(null)} className="text-gray-400 hover:text-white transition">✖</button>
|
||||||
✖
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm text-gray-400 mb-6">Le pare-feu HestiaCP bloque les injections de session directes. Un mot de passe de session <strong>jetable</strong> vient d'être généré sur le métal. Copiez-le et connectez-vous.</p>
|
||||||
<p className="text-sm text-gray-400 mb-6">
|
|
||||||
Le pare-feu HestiaCP bloque les injections de session directes. Un mot de passe de session <strong>jetable</strong> vient d'être généré sur le métal. Copiez-le et connectez-vous.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="space-y-4 mb-8">
|
<div className="space-y-4 mb-8">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Utilisateur</label>
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Utilisateur</label>
|
||||||
<div className="flex bg-gray-950 border border-gray-800 rounded p-3 justify-between items-center">
|
<div className="flex bg-gray-950 border border-gray-800 rounded p-3 justify-between items-center">
|
||||||
<span className="font-mono text-white">{ssoVault.username}</span>
|
<span className="font-mono text-white">{ssoVault.username}</span>
|
||||||
<button
|
<button onClick={() => navigator.clipboard.writeText(ssoVault.username)} className="text-xs bg-gray-800 hover:bg-gray-700 text-white px-3 py-1 rounded transition">Copier</button>
|
||||||
onClick={() => navigator.clipboard.writeText(ssoVault.username)}
|
|
||||||
className="text-xs bg-gray-800 hover:bg-gray-700 text-white px-3 py-1 rounded transition"
|
|
||||||
>Copier</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Clé Éphémère</label>
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Clé Éphémère</label>
|
||||||
<div className="flex bg-gray-950 border border-gray-800 rounded p-3 justify-between items-center">
|
<div className="flex bg-gray-950 border border-gray-800 rounded p-3 justify-between items-center">
|
||||||
<span className="font-mono text-green-400">{ssoVault.password}</span>
|
<span className="font-mono text-green-400">{ssoVault.password}</span>
|
||||||
<button
|
<button onClick={() => navigator.clipboard.writeText(ssoVault.password)} className="text-xs bg-cyan-600 hover:bg-cyan-500 text-white px-3 py-1 rounded transition shadow-lg shadow-cyan-500/30">Copier</button>
|
||||||
onClick={() => navigator.clipboard.writeText(ssoVault.password)}
|
|
||||||
className="text-xs bg-cyan-600 hover:bg-cyan-500 text-white px-3 py-1 rounded transition shadow-lg shadow-cyan-500/30"
|
|
||||||
>Copier</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<a href={ssoVault.url} target="_blank" rel="noopener noreferrer" onClick={() => setSsoVault(null)} className="block w-full bg-cyan-500 hover:bg-cyan-400 text-gray-950 font-bold py-3 text-center rounded transition font-mono tracking-widest uppercase">
|
||||||
|
Ouvrir le Terminal Hestia
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<a
|
{/* MODAL VPS PROXMOX */}
|
||||||
href={ssoVault.url}
|
{activeVpsModal && (
|
||||||
target="_blank"
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||||
rel="noopener noreferrer"
|
<div className="bg-gray-900 border border-cyan-500/50 rounded-lg shadow-2xl shadow-cyan-500/20 max-w-2xl w-full p-6 text-gray-200">
|
||||||
onClick={() => setSsoVault(null)}
|
<div className="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
|
||||||
className="w-full bg-cyan-500 hover:bg-cyan-400 text-gray-950 font-bold py-3 text-center rounded transition font-mono tracking-widest uppercase"
|
<h3 className="text-xl font-bold text-white tracking-wider flex items-center gap-3">
|
||||||
>
|
<Server className="w-6 h-6 text-cyan-400" />
|
||||||
Ouvrir le Terminal Hestia
|
{activeVpsModal.hostingDetails?.ip && activeVpsModal.hostingDetails.ip !== '127.0.0.1'
|
||||||
</a>
|
? "GESTION DE L'INSTANCE"
|
||||||
|
: "PHASE D'INITIALISATION"}
|
||||||
|
</h3>
|
||||||
|
<button onClick={() => setActiveVpsModal(null)} className="text-gray-400 hover:text-white transition text-xl">✖</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{activeVpsModal.hostingDetails?.ip && activeVpsModal.hostingDetails.ip !== '127.0.0.1' ? (
|
||||||
|
<VpsManager details={activeVpsModal.hostingDetails} />
|
||||||
|
) : (
|
||||||
|
<VpsDeployer
|
||||||
|
serviceId={activeVpsModal.id}
|
||||||
|
onDeploySuccess={(newDetails) => {
|
||||||
|
setActiveVpsModal({...activeVpsModal, hostingDetails: newDetails});
|
||||||
|
setServices(prev => prev.map(s => s.id === activeVpsModal.id ? {...s, hostingDetails: newDetails} : s));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const VpsDeployer = ({ serviceId }) => {
|
||||||
|
// Nouveaux états pour le Domaine et le Mot de passe
|
||||||
|
const [domain, setDomain] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
const [result, setResult] = useState(null);
|
||||||
|
|
||||||
|
const handleDeploy = async () => {
|
||||||
|
if (password.length < 8) {
|
||||||
|
alert("Le mot de passe doit faire au moins 8 caractères.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!domain.includes('.')) {
|
||||||
|
alert("Veuillez entrer un nom de domaine valide (ex: mon-projet.fr).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeploying(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// L'appel vers ton Pont PHP avec les nouvelles données
|
||||||
|
const response = await fetch('https://web.gise.be/custom_api/proxmox_create_vps.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
service_id: serviceId, // L'ID du produit dans FOSSBilling
|
||||||
|
domain: domain, // Le domaine choisi par le client
|
||||||
|
password: password
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
setResult(data);
|
||||||
|
} else {
|
||||||
|
alert("Erreur de déploiement : " + data.message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Erreur de connexion au serveur de provisionnement.");
|
||||||
|
} finally {
|
||||||
|
setIsDeploying(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-900 border border-cyan-500/30 p-6 rounded-lg text-gray-200 mt-4 shadow-xl">
|
||||||
|
<h3 className="text-xl font-mono text-cyan-400 mb-4">Enregistrement & Instanciation</h3>
|
||||||
|
|
||||||
|
{!result ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
Configuration système : <span className="text-white">Ubuntu 26.04 LTS (Docker Ready)</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Nom de domaine (Hostname) :</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={domain}
|
||||||
|
onChange={(e) => setDomain(e.target.value.toLowerCase())}
|
||||||
|
className="w-full bg-gray-950 border border-gray-700 text-white p-3 rounded font-mono focus:border-cyan-500 outline-none"
|
||||||
|
placeholder="ex: srv1.mon-domaine.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Mot de passe ROOT :</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full bg-gray-950 border border-gray-700 text-white p-3 rounded font-mono focus:border-cyan-500 outline-none"
|
||||||
|
placeholder="Créez un mot de passe fort"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleDeploy}
|
||||||
|
disabled={isDeploying}
|
||||||
|
className={`w-full py-3 mt-4 rounded font-bold font-mono tracking-widest uppercase transition ${isDeploying ? 'bg-gray-700 text-gray-400 cursor-not-allowed' : 'bg-cyan-600 hover:bg-cyan-500 text-white shadow-lg shadow-cyan-500/20'}`}
|
||||||
|
>
|
||||||
|
{isDeploying ? 'Synchronisation & Déploiement...' : 'Déployer le Serveur'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-green-900/20 border border-green-500/50 p-4 rounded text-green-400 text-center font-mono font-bold">
|
||||||
|
✓ INSTANCE ENREGISTRÉE ET OPÉRATIONNELLE
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between bg-gray-950 p-3 rounded border border-gray-800">
|
||||||
|
<span className="text-gray-400">Domaine lié:</span>
|
||||||
|
<span className="text-white">{result.domain}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between bg-gray-950 p-3 rounded border border-gray-800">
|
||||||
|
<span className="text-gray-400">Adresse IP Allouée:</span>
|
||||||
|
<span className="text-cyan-400 font-bold">{result.ip}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-center text-gray-500 mt-4">
|
||||||
|
Ce serveur est maintenant rattaché à votre compte. Vous pouvez vous y connecter en SSH via l'IP ou le domaine (une fois les DNS propagés).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VpsDeployer;
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
// Ce composant reçoit les détails du service depuis ton API FOSSBilling habituelle
|
||||||
|
const VpsManager = ({ serviceDetails }) => {
|
||||||
|
const [isActionPending, setIsActionPending] = useState(false);
|
||||||
|
|
||||||
|
// Fonction pour envoyer des ordres d'alimentation à Proxmox (via ton API)
|
||||||
|
const handlePowerAction = async (action) => {
|
||||||
|
if (!window.confirm(`Êtes-vous sûr de vouloir ${action} ce VPS ?`)) return;
|
||||||
|
setIsActionPending(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Tu devras créer un petit fichier 'proxmox_power.php' plus tard pour gérer ça
|
||||||
|
alert(`Signal d'alimentation "${action}" envoyé à l'hyperviseur.`);
|
||||||
|
// const response = await fetch('https://web.gise.be/custom_api/proxmox_power.php', { ... })
|
||||||
|
} catch (error) {
|
||||||
|
alert("Erreur de communication avec l'infrastructure.");
|
||||||
|
} finally {
|
||||||
|
setIsActionPending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Si Proxmox n'a pas encore fini de configurer, on affiche un loader
|
||||||
|
if (!serviceDetails.ip) {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-900 border border-cyan-500/30 p-6 rounded-lg text-gray-200 mt-4 text-center">
|
||||||
|
<div className="animate-pulse flex flex-col items-center">
|
||||||
|
<div className="h-8 w-8 border-4 border-cyan-500 border-t-transparent rounded-full animate-spin mb-4"></div>
|
||||||
|
<p className="text-cyan-400 font-mono tracking-widest text-sm">PROVISIONNEMENT EN COURS...</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-2">Votre serveur est en cours d'assemblage sur le VLAN 60. Cela prend environ 10 secondes.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-900 border border-cyan-500/50 shadow-xl shadow-cyan-500/10 p-6 rounded-lg text-gray-200 mt-4">
|
||||||
|
<div className="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
|
||||||
|
<h3 className="text-xl font-mono text-white flex items-center gap-3">
|
||||||
|
<span className="h-3 w-3 rounded-full bg-green-500 shadow-[0_0_10px_#22c55e]"></span>
|
||||||
|
NEXUS COMPUTE INSTANCE
|
||||||
|
</h3>
|
||||||
|
<span className="bg-gray-950 border border-gray-700 px-3 py-1 rounded text-xs text-gray-400 font-mono">
|
||||||
|
ID: {serviceDetails.id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
{/* BLOC RÉSEAU */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Adresse IP (VLAN 60)</label>
|
||||||
|
<div className="bg-gray-950 border border-gray-800 rounded p-3 font-mono text-green-400 font-bold">
|
||||||
|
{serviceDetails.ip}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Hostname Interne</label>
|
||||||
|
<div className="bg-gray-950 border border-gray-800 rounded p-3 font-mono text-gray-300 text-sm">
|
||||||
|
{serviceDetails.domain}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BLOC ACCÈS */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Utilisateur SSH</label>
|
||||||
|
<div className="bg-gray-950 border border-gray-800 rounded p-3 font-mono text-white">
|
||||||
|
root
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Mot de passe Root</label>
|
||||||
|
<div className="flex bg-gray-950 border border-gray-800 rounded p-3 justify-between items-center">
|
||||||
|
<span className="font-mono text-gray-400 text-sm">
|
||||||
|
{serviceDetails.password || serviceDetails.pass}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => navigator.clipboard.writeText(serviceDetails.password || serviceDetails.pass)}
|
||||||
|
className="text-xs bg-gray-800 hover:bg-gray-700 text-white px-3 py-1 rounded transition"
|
||||||
|
>Copier</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CONTRÔLES D'ALIMENTATION */}
|
||||||
|
<div className="bg-gray-950 rounded border border-gray-800 p-4">
|
||||||
|
<label className="block text-xs uppercase tracking-widest text-gray-500 mb-3">Contrôles d'Alimentation</label>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => handlePowerAction('redémarrer')}
|
||||||
|
disabled={isActionPending}
|
||||||
|
className="flex-1 bg-gray-800 hover:bg-gray-700 text-white text-sm py-2 rounded transition font-mono border border-gray-600"
|
||||||
|
>
|
||||||
|
↻ Reboot
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePowerAction('éteindre')}
|
||||||
|
disabled={isActionPending}
|
||||||
|
className="flex-1 bg-red-900/30 hover:bg-red-900/50 text-red-400 text-sm py-2 rounded transition font-mono border border-red-900/50"
|
||||||
|
>
|
||||||
|
■ Stop
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VpsManager;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// composant VpsServiceDetail.jsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
// Tu as déjà ces composants de la discussion précédente :
|
||||||
|
// import VpsDeployer from './VpsDeployer';
|
||||||
|
// import VpsManager from './VpsManager';
|
||||||
|
|
||||||
|
const VpsServiceDetail = ({ serviceDetails }) => {
|
||||||
|
|
||||||
|
// Le test logique : Si le service a une IP, il est déjà créé sur Proxmox.
|
||||||
|
const isProvisioned = serviceDetails.ip && serviceDetails.ip !== '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-4">Gestion VPS</h2>
|
||||||
|
|
||||||
|
{isProvisioned ? (
|
||||||
|
// Étape 4 (après création) : Le tableau de bord
|
||||||
|
<VpsManager serviceDetails={serviceDetails} />
|
||||||
|
) : (
|
||||||
|
// Étape 3 (avant création) : Le formulaire de déploiement
|
||||||
|
<div>
|
||||||
|
<div className="bg-yellow-900/30 border border-yellow-600/50 p-4 rounded mb-6 text-yellow-500">
|
||||||
|
<p className="font-bold">Votre VPS est prêt à être instancié !</p>
|
||||||
|
<p className="text-sm">La facturation est activée. Veuillez configurer les accès initiaux pour lancer la création sur l'infrastructure (VLAN 60).</p>
|
||||||
|
</div>
|
||||||
|
{/* On passe le service_id au composant Deployer */}
|
||||||
|
<VpsDeployer serviceId={serviceDetails.id} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VpsServiceDetail;
|
||||||
Reference in New Issue
Block a user