modification vps et web et add npm rules
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 45s
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 45s
This commit is contained in:
+305
-203
@@ -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}
|
||||||
</div>
|
</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>
|
) : (
|
||||||
|
<div className="flex gap-2">
|
||||||
{/* BLOC OUTILS MANAGÉS */}
|
<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} />
|
||||||
<div className="space-y-4">
|
<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">
|
||||||
<div>
|
{isUpdating ? 'SYNC...' : 'SAUVEGARDER'}
|
||||||
<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>
|
</button>
|
||||||
<p className="text-[10px] text-gray-600 mt-1 uppercase">• Utilisateur: root</p>
|
<button onClick={() => setIsEditingDomain(false)} className="bg-gray-800 text-white px-3 py-1.5 rounded text-xs hover:bg-gray-700">ANNULER</button>
|
||||||
<p className="text-[10px] text-gray-600 mt-1 uppercase">• Pass: NexusGise2026! (à modifier)</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
)}
|
||||||
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Console Système</label>
|
{isCustomDomain && (
|
||||||
<button
|
<div className="mt-4 border-t border-gray-900 pt-3 flex justify-between items-center">
|
||||||
onClick={() => setShowTerminal(true)}
|
<p className="text-[11px] text-gray-500">Domaine externe détecté. Sécurisez après pointage DNS.</p>
|
||||||
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"
|
<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</>}
|
||||||
<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>
|
</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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-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>
|
<div>
|
||||||
<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>
|
<label className="block text-xs uppercase tracking-widest text-cyan-500 mb-1">Explorateur de Fichiers</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" /> Ouvrir l'explorateur</span>
|
||||||
|
<ExternalLink className="w-4 h-4 text-gray-600 group-hover:text-cyan-400" />
|
||||||
|
</button>
|
||||||
|
</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>{">_"}</span> Ouvrir le Terminal</span>
|
||||||
|
<Play className="w-4 h-4 text-cyan-600 group-hover:text-cyan-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</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]">
|
||||||
{/* Le conteneur iFrame qui charge TTYD (xterm.js) */}
|
<iframe src={terminalUrl} className="w-full h-full border-none" title="Terminal" />
|
||||||
<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,76 +438,66 @@ 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) {
|
||||||
|
const detailedServices = await Promise.all(
|
||||||
|
data.list.map(async (order) => {
|
||||||
|
let hDetails = null;
|
||||||
|
if (order.status === 'active' && (order.plugin === 'hosting' || (order.title || '').toLowerCase().includes('vps'))) {
|
||||||
|
try { hDetails = await getHostingServiceDetails(order.id); }
|
||||||
|
catch (e) { console.warn(`Détails inaccessibles pour ${order.id}:`, e); }
|
||||||
|
}
|
||||||
|
return { ...order, hostingDetails: hDetails };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (data.list && data.list.length > 0) {
|
const filteredServices = detailedServices.filter(s => {
|
||||||
const detailedServices = await Promise.all(
|
const type = (s.type || '').toLowerCase();
|
||||||
data.list.map(async (order) => {
|
const title = (s.title || '').toLowerCase();
|
||||||
let hDetails = null;
|
const isGhostProduct = type === 'domain' || title.includes('domain') || title.includes('domaine') || title.includes('enregistrement');
|
||||||
if (order.plugin === 'hosting' || (order.title||'').toLowerCase().includes('vps')) {
|
return (s.status === 'active' || s.status === 'pending_setup') && !isGhostProduct;
|
||||||
try { hDetails = await getHostingServiceDetails(order.id); } catch(e){}
|
});
|
||||||
}
|
|
||||||
return { ...order, hostingDetails: hDetails };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredServices = detailedServices.filter(s => {
|
setServices(filteredServices);
|
||||||
const type = (s.type || '').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');
|
|
||||||
return (s.status === 'active' || s.status === 'pending_setup') && !isGhostProduct;
|
|
||||||
});
|
|
||||||
|
|
||||||
setServices(filteredServices);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message || "Impossible de charger la télémétrie des services.");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
} catch (err) {
|
||||||
fetchServices();
|
setError(err.message || "Impossible de charger la télémétrie des services.");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
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 serviceId={activeServiceModal.data.id} onDeploySuccess={() => { setActiveServiceModal(null); fetchServices(); }} />
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<VpsDeployer
|
<WebManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onOpenSso={setSsoVault} />
|
||||||
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user