modification vps et web et add npm rules
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 45s

This commit is contained in:
maximus
2026-06-22 13:26:54 +02:00
parent e611cdcfc1
commit bdcb6cb3f9
+277 -175
View File
@@ -1,23 +1,20 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { getMyServices, getServiceDetails, getHostingServiceDetails, resetHostingPassword } 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, Play } from 'lucide-react'; import { Server, Database, Cloud, Globe, Folder, Trash2, ExternalLink, Loader, Plus, Play, Lock } from 'lucide-react';
// ============================================================================ // ============================================================================
// COMPOSANT ISOLÉ : INSTANCE CARD // COMPOSANT 1 : INSTANCE CARD (Mode PaaS Pur - IP Masquée)
// ============================================================================ // ============================================================================
const InstanceCard = ({ service, vpcs, onAssignVpc, onRemoveVpc, onOpenConsole, isConnecting }) => { const InstanceCard = ({ service, vpcs, onAssignVpc, onRemoveVpc, onOpenConsole, isConnecting }) => {
const titleLower = (service.title || '').toLowerCase(); const titleLower = (service.title || '').toLowerCase();
const isVPS = titleLower.includes('vps') || titleLower.includes('compute'); const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
const isCloud = titleLower.includes('cloud') || titleLower.includes('nextcloud'); const isCloud = titleLower.includes('cloud') || titleLower.includes('nextcloud');
const isDB = titleLower.includes('db') || titleLower.includes('base') || titleLower.includes('sql'); const isDB = titleLower.includes('db') || titleLower.includes('base') || titleLower.includes('sql');
const isWeb = !isVPS && !isCloud && !isDB;
// 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 !== ''; 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 buttonText = "CONSOLE D'ADMINISTRATION";
let ButtonIcon = ExternalLink; let ButtonIcon = ExternalLink;
let buttonStyle = "bg-cyan-400/10 hover:bg-cyan-400 text-cyan-400 hover:text-gray-900 border-cyan-400"; let buttonStyle = "bg-cyan-400/10 hover:bg-cyan-400 text-cyan-400 hover:text-gray-900 border-cyan-400";
@@ -31,9 +28,12 @@ const InstanceCard = ({ service, vpcs, onAssignVpc, onRemoveVpc, onOpenConsole,
ButtonIcon = Play; ButtonIcon = Play;
buttonStyle = "bg-yellow-500/10 text-yellow-500 hover:bg-yellow-500 hover:text-gray-900 border-yellow-500/50"; buttonStyle = "bg-yellow-500/10 text-yellow-500 hover:bg-yellow-500 hover:text-gray-900 border-yellow-500/50";
} }
} else if (isWeb) {
buttonText = "GÉRER L'HÉBERGEMENT";
ButtonIcon = Globe;
buttonStyle = "bg-emerald-400/10 hover:bg-emerald-400 text-emerald-400 hover:text-gray-900 border-emerald-400";
} else if (isCloud) { } else if (isCloud) {
buttonText = "ACCÉDER AU CLOUD"; 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"; buttonStyle = "bg-blue-400/10 hover:bg-blue-400 text-blue-400 hover:text-gray-900 border-blue-400";
} else if (isDB) { } else if (isDB) {
buttonText = "PHPMYADMIN / CLUSTER"; buttonText = "PHPMYADMIN / CLUSTER";
@@ -48,29 +48,20 @@ const InstanceCard = ({ service, vpcs, onAssignVpc, onRemoveVpc, onOpenConsole,
return <Globe className="w-8 h-8 text-emerald-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 baseTitle = (service.title || '').split(/(?: for | pour )/i)[0].trim();
const shortTitle = baseTitle.split(' ').slice(0, 2).join(' '); 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 titleMatch = (service.title || '').match(/(?: for | pour )(.+)$/i);
const domainFromTitle = titleMatch ? titleMatch[1].trim() : null; 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' let displayDomain = service.hostingDetails?.domain && service.hostingDetails.domain !== '127.0.0.1'
? service.hostingDetails.domain ? service.hostingDetails.domain
: domainFromTitle || service.domain; : domainFromTitle || service.domain;
if (!displayDomain || displayDomain === '127.0.0.1') { if (!displayDomain || displayDomain === '127.0.0.1') displayDomain = null;
displayDomain = null;
}
return ( 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 className="bg-gray-900 border border-gray-800 rounded-xl p-5 flex flex-col justify-between hover:border-gray-700 transition-all shadow-lg">
<div> <div>
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<div className="p-2 bg-black/40 rounded-lg"> <div className="p-2 bg-black/40 rounded-lg">
@@ -89,16 +80,13 @@ const InstanceCard = ({ service, vpcs, onAssignVpc, onRemoveVpc, onOpenConsole,
{shortTitle} {shortTitle}
</h3> </h3>
{/* Espace fixe pour le domaine (remplace l'ancien CMD: #id) */} <div className="flex flex-col gap-1 mt-2 mb-4 h-8 justify-center">
<div className="h-6 mt-1 mb-4 flex items-center">
{displayDomain ? ( {displayDomain ? (
<p className="text-cyan-400 text-xs font-mono truncate" title={displayDomain}> <p className={`${isVPS ? 'text-cyan-400' : 'text-emerald-400'} text-xs font-mono truncate`} title={displayDomain}>
{displayDomain} {displayDomain}
</p> </p>
) : ( ) : (
<p className="text-gray-600 text-xs font-mono italic"> <p className="text-gray-600 text-xs font-mono italic">En attente de déploiement</p>
Non configuré
</p>
)} )}
</div> </div>
</div> </div>
@@ -139,7 +127,7 @@ const InstanceCard = ({ service, vpcs, onAssignVpc, onRemoveVpc, onOpenConsole,
}; };
// ============================================================================ // ============================================================================
// MODAL : VPS DEPLOYER // COMPOSANT 2 : VPS DEPLOYER (Instanciation)
// ============================================================================ // ============================================================================
const VpsDeployer = ({ serviceId, onDeploySuccess }) => { const VpsDeployer = ({ serviceId, onDeploySuccess }) => {
const [domain, setDomain] = useState(''); const [domain, setDomain] = useState('');
@@ -173,14 +161,14 @@ const VpsDeployer = ({ serviceId, onDeploySuccess }) => {
return ( return (
<div> <div>
<div className="bg-yellow-900/20 border border-yellow-600/30 p-4 rounded mb-6 text-yellow-500/80 text-sm"> <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/> <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. 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>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Hostname personnalisé (Optionnel)</label> <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" /> <input type="text" value={domain} onChange={(e) => setDomain(e.target.value.toLowerCase())} placeholder="Laissez vide pour le défaut" 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>
<div> <div>
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Mot de passe Root (Requis)</label> <label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Mot de passe Root (Requis)</label>
@@ -195,111 +183,118 @@ const VpsDeployer = ({ serviceId, onDeploySuccess }) => {
}; };
// ============================================================================ // ============================================================================
// MODAL : VPS MANAGER (Modèle PaaS Complet) // COMPOSANT 3 : VPS MANAGER (Day-2 Operations)
// ============================================================================ // ============================================================================
const VpsManager = ({ details }) => { const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
const [showTerminal, setShowTerminal] = useState(false); const [showTerminal, setShowTerminal] = useState(false);
const [isEditingDomain, setIsEditingDomain] = useState(false);
const [newDomain, setNewDomain] = useState(details.domain);
const [isUpdating, setIsUpdating] = useState(false);
const [sslStatus, setSslStatus] = useState(null);
// Les URLs de tes outils internes const isInternal = details.domain && details.domain.endsWith('.gise.be');
const fileManagerUrl = `https://file.${details.domain}`; const isCustomDomain = details.domain && !isInternal;
const terminalUrl = `https://terminal.${details.domain}`;
const fileManagerUrl = isInternal ? `https://file-${details.domain}` : `http://${details.domain}:8080`;
const terminalUrl = isInternal ? `https://terminal-${details.domain}` : `http://${details.domain}:8081`;
const handleUpdateDomain = async () => {
if (!newDomain || newDomain === details.domain) return;
setIsUpdating(true);
try {
const response = await fetch('https://web.gise.be/custom_api/update_service_domain.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ order_id: orderId, new_domain: newDomain })
});
const data = await response.json();
if (data.status === 'success') {
setIsEditingDomain(false);
if (onRefresh) onRefresh();
alert("Domaine VPS mis à jour avec succès sur l'infrastructure !");
} else alert("Erreur: " + data.error);
} catch (err) {
alert("Erreur de connexion avec l'API.");
} finally { setIsUpdating(false); }
};
const handleGenerateSSL = async () => {
setSslStatus('loading');
try {
const response = await fetch('https://web.gise.be/custom_api/generate_custom_ssl.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ domain: details.domain })
});
const data = await response.json();
if (data.status === 'success') {
setSslStatus('success');
alert("Certificat SSL Let's Encrypt généré et activé avec succès !");
} else {
setSslStatus('error');
alert("Échec de la validation DNS. Vérifiez que votre domaine pointe bien vers notre IP.");
}
} catch (e) { setSslStatus('error'); }
};
return ( return (
<div> <div>
{!showTerminal ? ( {!showTerminal ? (
<> <>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> <div className="mb-6 bg-gray-950 border border-gray-800 rounded p-4">
{/* BLOC RÉSEAU */} <label className="block text-xs uppercase tracking-widest text-cyan-500 mb-2">Domaine de l'instance VPS</label>
<div className="space-y-4"> {!isEditingDomain ? (
<div> <div className="flex justify-between items-center">
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Identifiant Réseau</label> <span className="font-mono text-xl text-green-400 font-bold flex items-center gap-2">
<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-3 w-3 rounded-full bg-green-500 shadow-[0_0_10px_#22c55e]"></span>
<span className="h-2 w-2 rounded-full bg-green-500 shadow-[0_0_8px_#22c55e]"></span>
{details.domain} {details.domain}
</span>
<button onClick={() => setIsEditingDomain(true)} className="text-xs border border-gray-700 hover:border-cyan-500 text-gray-400 hover:text-cyan-400 px-3 py-1.5 rounded transition">MODIFIER</button>
</div> </div>
) : (
<div className="flex gap-2">
<input type="text" value={newDomain} onChange={(e) => setNewDomain(e.target.value.toLowerCase())} className="flex-1 bg-black border border-gray-700 rounded px-3 py-1.5 text-white font-mono text-sm focus:border-cyan-500 outline-none" disabled={isUpdating} />
<button onClick={handleUpdateDomain} disabled={isUpdating} className="bg-cyan-500 text-gray-900 px-4 py-1.5 rounded text-xs font-bold hover:bg-cyan-400">
{isUpdating ? 'SYNC...' : 'SAUVEGARDER'}
</button>
<button onClick={() => setIsEditingDomain(false)} className="bg-gray-800 text-white px-3 py-1.5 rounded text-xs hover:bg-gray-700">ANNULER</button>
</div> </div>
)}
{isCustomDomain && (
<div className="mt-4 border-t border-gray-900 pt-3 flex justify-between items-center">
<p className="text-[11px] text-gray-500">Domaine externe détecté. Sécurisez après pointage DNS.</p>
<button onClick={handleGenerateSSL} disabled={sslStatus === 'loading'} className="text-xs bg-purple-600/20 hover:bg-purple-600 text-purple-400 hover:text-white border border-purple-500/30 px-3 py-1 rounded transition flex items-center gap-1">
{sslStatus === 'loading' ? 'GÉNÉRATION...' : <><Lock className="w-3 h-3" /> GÉNÉRER SSL</>}
</button>
</div>
)}
</div> </div>
{/* BLOC OUTILS MANAGÉS */} <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div className="space-y-4">
<div> <div>
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Fichiers (FileBrowser)</label> <label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Explorateur de Fichiers</label>
<button <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">
onClick={() => window.open(fileManagerUrl, '_blank')} <span className="font-mono text-gray-300 text-sm flex items-center gap-2"><Folder className="w-4 h-4 text-cyan-500" /> Ouvrir l'explorateur</span>
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" /> <ExternalLink className="w-4 h-4 text-gray-600 group-hover:text-cyan-400" />
</button> </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>
<div> <div>
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Console Système</label> <label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Console Système</label>
<button <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">
onClick={() => setShowTerminal(true)} <span className="font-mono text-cyan-400 text-sm flex items-center gap-2"><span>{">_"}</span> Ouvrir le Terminal</span>
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" /> <Play className="w-4 h-4 text-cyan-600 group-hover:text-cyan-400" />
</button> </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>
</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>
<div className="animate-in fade-in duration-300">
<div className="flex justify-between items-center mb-4 border-b border-gray-800 pb-2"> <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="text-cyan-400 font-mono text-sm tracking-widest">NEXUS TERMINAL ({details.domain})</span>
<span className="h-2 w-2 bg-cyan-400 rounded-full animate-pulse"></span> <button onClick={() => setShowTerminal(false)} className="text-xs bg-red-900/30 text-white px-3 py-1 rounded">Fermer</button>
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>
</div> <div className="bg-black border border-gray-800 rounded h-[400px]">
<iframe src={terminalUrl} className="w-full h-full border-none" title="Terminal" />
{/* 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> </div>
)} )}
@@ -307,6 +302,132 @@ const VpsManager = ({ details }) => {
); );
}; };
// ============================================================================
// COMPOSANT 4 : WEB MANAGER (HestiaCP & Day-2 Operations)
// ============================================================================
const WebManager = ({ details, orderId, onRefresh, onOpenSso }) => {
const [isEditingDomain, setIsEditingDomain] = useState(false);
const [newDomain, setNewDomain] = useState(details.domain);
const [isUpdating, setIsUpdating] = useState(false);
const [sslStatus, setSslStatus] = useState(null);
const [isGeneratingSso, setIsGeneratingSso] = useState(false);
const isCustomDomain = details.domain && !details.domain.endsWith('.gise.be');
const siteUrl = (isCustomDomain && sslStatus !== 'success') ? `http://${details.domain}` : `https://${details.domain}`;
const handleUpdateDomain = async () => {
if (!newDomain || newDomain === details.domain) return;
setIsUpdating(true);
try {
const response = await fetch('https://web.gise.be/custom_api/update_service_domain.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ order_id: orderId, new_domain: newDomain })
});
const data = await response.json();
if (data.status === 'success') {
setIsEditingDomain(false);
if (onRefresh) onRefresh();
alert("Domaine web mis à jour avec succès sur le serveur HestiaCP !");
} else alert("Erreur: " + data.error);
} catch (err) {
alert("Erreur de connexion avec l'API.");
} finally { setIsUpdating(false); }
};
const handleGenerateSSL = async () => {
setSslStatus('loading');
try {
const response = await fetch('https://web.gise.be/custom_api/generate_custom_ssl.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ domain: details.domain })
});
const data = await response.json();
if (data.status === 'success') {
setSslStatus('success');
alert("Certificat SSL Let's Encrypt généré !");
} else {
setSslStatus('error');
alert("Échec Let's Encrypt. Vérifiez vos DNS.");
}
} catch (e) { setSslStatus('error'); }
};
const handleOpenPanel = async () => {
setIsGeneratingSso(true);
try {
const secureHash = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-4).toUpperCase();
const rollingPassword = `Nx${secureHash}`;
await resetHostingPassword(orderId, rollingPassword);
// On déclenche le modal SSO du composant parent
onOpenSso({ username: details.username, password: rollingPassword, url: 'https://panel.gise.be/login/' });
} catch (err) {
alert("Erreur lors de la génération du SSO HestiaCP.");
} finally {
setIsGeneratingSso(false);
}
};
return (
<div>
<div className="mb-6 bg-gray-950 border border-gray-800 rounded p-4">
<label className="block text-xs uppercase tracking-widest text-emerald-500 mb-2">Domaine du Site Web</label>
{!isEditingDomain ? (
<div className="flex justify-between items-center">
<span className="font-mono text-xl text-emerald-400 font-bold flex items-center gap-2">
<span className="h-3 w-3 rounded-full bg-emerald-500 shadow-[0_0_10px_#10b981]"></span>
{details.domain}
</span>
<button onClick={() => setIsEditingDomain(true)} className="text-xs border border-gray-700 hover:border-emerald-500 text-gray-400 hover:text-emerald-400 px-3 py-1.5 rounded transition">MODIFIER</button>
</div>
) : (
<div className="flex gap-2">
<input type="text" value={newDomain} onChange={(e) => setNewDomain(e.target.value.toLowerCase())} className="flex-1 bg-black border border-gray-700 rounded px-3 py-1.5 text-white font-mono text-sm focus:border-emerald-500 outline-none" disabled={isUpdating} />
<button onClick={handleUpdateDomain} disabled={isUpdating} className="bg-emerald-500 text-gray-900 px-4 py-1.5 rounded text-xs font-bold hover:bg-emerald-400">
{isUpdating ? 'SYNC...' : 'SAUVEGARDER'}
</button>
<button onClick={() => setIsEditingDomain(false)} className="bg-gray-800 text-white px-3 py-1.5 rounded text-xs hover:bg-gray-700">ANNULER</button>
</div>
)}
{isCustomDomain && (
<div className="mt-4 border-t border-gray-900 pt-3 flex justify-between items-center">
<p className="text-[11px] text-gray-500">Domaine personnalisé détecté. Sécurisez après pointage DNS.</p>
<button onClick={handleGenerateSSL} disabled={sslStatus === 'loading'} className="text-xs bg-purple-600/20 hover:bg-purple-600 text-purple-400 hover:text-white border border-purple-500/30 px-3 py-1 rounded transition flex items-center gap-1">
{sslStatus === 'loading' ? 'GÉNÉRATION...' : <><Lock className="w-3 h-3" /> GÉNÉRER SSL</>}
</button>
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div>
<label className="block text-xs uppercase tracking-widest text-emerald-500 mb-1">Visiter le Site</label>
<button onClick={() => window.open(siteUrl, '_blank')} className="w-full bg-gray-950 hover:bg-gray-900 border border-gray-800 hover:border-emerald-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"><Globe className="w-4 h-4 text-emerald-500" /> Ouvrir dans le navigateur</span>
<ExternalLink className="w-4 h-4 text-gray-600 group-hover:text-emerald-400" />
</button>
</div>
<div>
<label className="block text-xs uppercase tracking-widest text-emerald-500 mb-1">Panel de Contrôle</label>
<button onClick={handleOpenPanel} disabled={isGeneratingSso} className="w-full bg-emerald-900/20 hover:bg-emerald-900/40 border border-emerald-800/50 hover:border-emerald-500 rounded p-3 flex justify-between items-center transition group disabled:opacity-50">
{isGeneratingSso ? (
<span className="font-mono text-emerald-400 text-sm flex items-center gap-2"><Loader className="w-4 h-4 animate-spin" /> GÉNÉRATION SSO...</span>
) : (
<>
<span className="font-mono text-emerald-400 text-sm flex items-center gap-2"><Lock className="w-4 h-4" /> Gérer l'hébergement</span>
<ExternalLink className="w-4 h-4 text-emerald-600 group-hover:text-emerald-400" />
</>
)}
</button>
</div>
</div>
</div>
);
};
// ============================================================================ // ============================================================================
// COMPOSANT PRINCIPAL : SERVICES // COMPOSANT PRINCIPAL : SERVICES
// ============================================================================ // ============================================================================
@@ -317,21 +438,21 @@ export default function Services() {
const [newVpcName, setNewVpcName] = useState(""); const [newVpcName, setNewVpcName] = useState("");
const [isConnecting, setIsConnecting] = useState(null); const [isConnecting, setIsConnecting] = useState(null);
const [ssoVault, setSsoVault] = useState(null); const [ssoVault, setSsoVault] = useState(null);
const [activeVpsModal, setActiveVpsModal] = useState(null); const [activeServiceModal, setActiveServiceModal] = useState(null); // Gère VPS et WEB
const { vpcs, createVPC, deleteVPC, assignToVPC, removeFromVPC } = useVPC(); const { vpcs, createVPC, deleteVPC, assignToVPC, removeFromVPC } = useVPC();
useEffect(() => { // Fonction extraite pour pouvoir être rappelée par les Managers
const fetchServices = async () => { const fetchServices = useCallback(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) {
const detailedServices = await Promise.all( const detailedServices = await Promise.all(
data.list.map(async (order) => { data.list.map(async (order) => {
let hDetails = null; let hDetails = null;
if (order.plugin === 'hosting' || (order.title||'').toLowerCase().includes('vps')) { if (order.status === 'active' && (order.plugin === 'hosting' || (order.title || '').toLowerCase().includes('vps'))) {
try { hDetails = await getHostingServiceDetails(order.id); } catch(e){} try { hDetails = await getHostingServiceDetails(order.id); }
catch (e) { console.warn(`Détails inaccessibles pour ${order.id}:`, e); }
} }
return { ...order, hostingDetails: hDetails }; return { ...order, hostingDetails: hDetails };
}) })
@@ -340,7 +461,6 @@ export default function Services() {
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();
// Ajout du mot-clé "enregistrement" au filtre d'exclusion
const isGhostProduct = type === 'domain' || title.includes('domain') || title.includes('domaine') || title.includes('enregistrement'); const isGhostProduct = type === 'domain' || title.includes('domain') || title.includes('domaine') || title.includes('enregistrement');
return (s.status === 'active' || s.status === 'pending_setup') && !isGhostProduct; return (s.status === 'active' || s.status === 'pending_setup') && !isGhostProduct;
}); });
@@ -352,41 +472,32 @@ export default function Services() {
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
};
fetchServices();
}, []); }, []);
useEffect(() => {
fetchServices();
}, [fetchServices]);
const handleOpenConsole = async (service) => { const handleOpenConsole = async (service) => {
const titleLower = (service.title || '').toLowerCase(); const titleLower = (service.title || '').toLowerCase();
const isVPS = titleLower.includes('vps') || titleLower.includes('compute'); const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
const isCloud = titleLower.includes('cloud') || titleLower.includes('nextcloud'); const isCloud = titleLower.includes('cloud') || titleLower.includes('nextcloud');
const isDB = titleLower.includes('db') || titleLower.includes('base') || titleLower.includes('sql'); const isDB = titleLower.includes('db') || titleLower.includes('base') || titleLower.includes('sql');
const isWeb = !isVPS && !isCloud && !isDB;
setIsConnecting(service.id); setIsConnecting(service.id);
try { try {
if (isVPS) { if (isCloud) {
const freshDetails = await getHostingServiceDetails(service.id);
setActiveVpsModal({ ...service, hostingDetails: freshDetails });
} else if (isCloud) {
// Redirection Cloud
const titleMatch = (service.title || '').match(/(?: for | pour )(.+)$/i); const titleMatch = (service.title || '').match(/(?: for | pour )(.+)$/i);
const domainUrl = titleMatch ? titleMatch[1].trim() : service.domain; const domainUrl = titleMatch ? titleMatch[1].trim() : service.domain;
if(domainUrl) window.open(`https://${domainUrl}`, '_blank'); if (domainUrl) window.open(`https://${domainUrl}`, '_blank');
} else if (isDB) { } else if (isDB) {
// Redirection Base de données
window.open('https://pma.gise.be/', '_blank'); window.open('https://pma.gise.be/', '_blank');
} else { } else if (isVPS || isWeb) {
// Redirection Web / HestiaCP // Pour VPS et Web, on récupère les détails frais et on ouvre le Modal Générique
const hostingDetails = await getHostingServiceDetails(service.id); const freshDetails = await getHostingServiceDetails(service.id);
const username = hostingDetails.username; setActiveServiceModal({ type: isVPS ? 'vps' : 'web', data: service, details: freshDetails });
if (!username) throw new Error("Infrastructure non synchronisée avec le métal.");
const secureHash = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-4).toUpperCase();
const rollingPassword = `Nx${secureHash}`;
await resetHostingPassword(service.id, rollingPassword);
setSsoVault({ username: username, password: rollingPassword, url: 'https://panel.gise.be/login/' });
} }
} catch (err) { } catch (err) {
alert("Échec du protocole : " + err.message); alert("Échec du protocole : " + err.message);
@@ -441,13 +552,9 @@ export default function Services() {
) : ( ) : (
vpcServices.map(service => ( vpcServices.map(service => (
<InstanceCard <InstanceCard
key={service.id} key={service.id} service={service} vpcs={vpcs}
service={service} onAssignVpc={assignToVPC} onRemoveVpc={removeFromVPC}
vpcs={vpcs} onOpenConsole={handleOpenConsole} isConnecting={isConnecting}
onAssignVpc={assignToVPC}
onRemoveVpc={removeFromVPC}
onOpenConsole={handleOpenConsole}
isConnecting={isConnecting}
/> />
)) ))
)} )}
@@ -469,13 +576,9 @@ export default function Services() {
) : ( ) : (
freeServices.map(service => ( freeServices.map(service => (
<InstanceCard <InstanceCard
key={service.id} key={service.id} service={service} vpcs={vpcs}
service={service} onAssignVpc={assignToVPC} onRemoveVpc={removeFromVPC}
vpcs={vpcs} onOpenConsole={handleOpenConsole} isConnecting={isConnecting}
onAssignVpc={assignToVPC}
onRemoveVpc={removeFromVPC}
onOpenConsole={handleOpenConsole}
isConnecting={isConnecting}
/> />
)) ))
)} )}
@@ -484,61 +587,60 @@ export default function Services() {
{/* MODAL 1 : HESTIACP SSO VAULT */} {/* 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-[60] 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-emerald-500/50 rounded-lg shadow-2xl shadow-emerald-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-emerald-400 font-mono tracking-wider">ACCÈS AUTORISÉ</h3>
<button onClick={() => setSsoVault(null)} className="text-gray-400 hover:text-white transition">✖</button> <button onClick={() => setSsoVault(null)} className="text-gray-400 hover:text-white transition">✖</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 directes. Un mot de passe <strong>jetable</strong> a été généré. 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-emerald-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 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> <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-emerald-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-emerald-400">{ssoVault.password}</span>
<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> <button onClick={() => navigator.clipboard.writeText(ssoVault.password)} className="text-xs bg-emerald-600 hover:bg-emerald-500 text-white px-3 py-1 rounded transition">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"> <a href={ssoVault.url} target="_blank" rel="noopener noreferrer" onClick={() => setSsoVault(null)} className="block w-full bg-emerald-500 hover:bg-emerald-400 text-gray-950 font-bold py-3 text-center rounded transition font-mono tracking-widest uppercase">
Ouvrir le Terminal Hestia Ouvrir le Panel Web
</a> </a>
</div> </div>
</div> </div>
)} )}
{/* MODAL 2 : GESTIONNAIRE DE SERVICE GÉNÉRIQUE (VPS & WEB) */}
{/* MODAL VPS PROXMOX */} {activeServiceModal && (
{activeVpsModal && (
<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-2xl w-full p-6 text-gray-200"> <div className={`bg-gray-900 border rounded-lg shadow-2xl max-w-2xl w-full p-6 text-gray-200 ${activeServiceModal.type === 'vps' ? 'border-cyan-500/50 shadow-cyan-500/20' : 'border-emerald-500/50 shadow-emerald-500/20'}`}>
<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">
<h3 className="text-xl font-bold text-white tracking-wider flex items-center gap-3"> <h3 className="text-xl font-bold text-white tracking-wider flex items-center gap-3">
<Server className="w-6 h-6 text-cyan-400" /> {activeServiceModal.type === 'vps' ? (
{activeVpsModal.hostingDetails?.ip && activeVpsModal.hostingDetails.ip !== '127.0.0.1' <><Server className="w-6 h-6 text-cyan-400" /> {activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? "GESTION DE L'INSTANCE" : "PHASE D'INITIALISATION"}</>
? "GESTION DE L'INSTANCE" ) : (
: "PHASE D'INITIALISATION"} <><Globe className="w-6 h-6 text-emerald-400" /> GESTION DE L'HÉBERGEMENT WEB</>
)}
</h3> </h3>
<button onClick={() => setActiveVpsModal(null)} className="text-gray-400 hover:text-white transition text-xl"></button> <button onClick={() => setActiveServiceModal(null)} className="text-gray-400 hover:text-white transition text-xl"></button>
</div> </div>
{activeVpsModal.hostingDetails?.ip && activeVpsModal.hostingDetails.ip !== '127.0.0.1' ? ( {/* ROUTAGE DU COMPOSANT INTERNE SELON L'ÉTAT DU SERVICE */}
<VpsManager details={activeVpsModal.hostingDetails} /> {activeServiceModal.type === 'vps' ? (
activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? (
<VpsManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} />
) : ( ) : (
<VpsDeployer <VpsDeployer serviceId={activeServiceModal.data.id} onDeploySuccess={() => { setActiveServiceModal(null); fetchServices(); }} />
serviceId={activeVpsModal.id} )
onDeploySuccess={(newDetails) => { ) : (
setActiveVpsModal({...activeVpsModal, hostingDetails: newDetails}); <WebManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onOpenSso={setSsoVault} />
setServices(prev => prev.map(s => s.id === activeVpsModal.id ? {...s, hostingDetails: newDetails} : s));
}}
/>
)} )}
</div> </div>
</div> </div>