import { useState, useEffect, useCallback } from 'react';
import { getMyServices, getServiceDetails, getHostingServiceDetails, resetHostingPassword } from '../../services/api';
import { useVPC } from '../../services/useVPC';
import { Server, Database, Cloud, Globe, Folder, Trash2, ExternalLink, Loader, Plus, Play, Lock, Maximize2 } from 'lucide-react';
// ============================================================================
// COMPOSANT 0 : MODAL DE NOTIFICATION (Remplace les alert() natifs)
// ============================================================================
const NotificationModal = ({ notification, onClose }) => {
if (!notification) return null;
const isError = notification.type === 'error';
return (
{notification.title.toUpperCase()}
{notification.message}
COMPRIS
);
};
// ============================================================================
// COMPOSANT 1 : INSTANCE CARD (Mode PaaS Pur - IP Masquée)
// ============================================================================
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');
const isWeb = !isVPS && !isCloud && !isDB;
const hasIP = service.hostingDetails?.ip && service.hostingDetails.ip !== '127.0.0.1' && service.hostingDetails.ip !== '';
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 (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) {
buttonText = "ACCÉDER AU CLOUD";
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 ;
if (isCloud) return ;
if (isDB) return ;
return ;
};
const baseTitle = (service.title || '').split(/(?: for | pour )/i)[0].trim();
const shortTitle = baseTitle.split(' ').slice(0, 2).join(' ');
const titleMatch = (service.title || '').match(/(?: for | pour )(.+)$/i);
const domainFromTitle = titleMatch ? titleMatch[1].trim() : null;
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 (
{getServiceIcon()}
{service.status === 'active' ? (
{isVPS && !hasIP ? 'AWAITING INIT' : 'ONLINE'}
) : (
DEPLOYING
)}
{shortTitle}
{displayDomain ? (
{displayDomain}
) : (
En attente de déploiement
)}
Projet VPC:
{
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"}
>
-- Libre --
{vpcs.map(vpc => (
{vpc.name}
))}
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 ? (
<>CONNEXION... >
) : (
<>{buttonText} >
)}
);
};
// ============================================================================
// COMPOSANT 2 : VPS DEPLOYER (Instanciation)
// ============================================================================
const VpsDeployer = ({ serviceId, onDeploySuccess, onAlert }) => {
const [domain, setDomain] = useState('');
const [password, setPassword] = useState('');
const [isDeploying, setIsDeploying] = useState(false);
const handleDeploy = async () => {
if (password.length < 8) { onAlert("Sécurité", "Le mot de passe doit faire au moins 8 caractères.", "error"); 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 {
onAlert("Échec", "Erreur de provisionnement : " + (data.message || 'Inconnue'), "error");
}
} catch (err) {
onAlert("Erreur Réseau", "Erreur de communication avec l'API Proxmox Gateway.", "error");
} finally {
setIsDeploying(false);
}
};
return (
PHASE D'INITIALISATION REQUISE
La facturation est activée. Veuillez définir un mot de passe Root pour lancer la création de l'instance sur l'hyperviseur.
{isDeploying ? 'Fabrication sur le Métal en cours...' : 'Lancer l\'Instanciation'}
);
};
// ============================================================================
// COMPOSANT 3 : VPS MANAGER (Day-2 Operations)
// ============================================================================
const VpsManager = ({ details, orderId, onRefresh, onAlert }) => {
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);
const isInternal = details.domain && details.domain.endsWith('.gise.be');
const isCustomDomain = details.domain && !isInternal;
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();
onAlert("Succès", "Domaine VPS mis à jour avec succès sur l'infrastructure !", "success");
} else onAlert("Erreur", data.error, "error");
} catch (err) {
onAlert("Erreur", "Erreur de connexion avec l'API.", "error");
} 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');
onAlert("SSL Activé", "Certificat SSL Let's Encrypt généré et activé avec succès !", "success");
} else {
setSslStatus('error');
onAlert("Challenge DNS Échoué", "Vérifiez que votre domaine pointe bien vers notre IP publique.", "error");
}
} catch (e) { setSslStatus('error'); }
};
return (
{!showTerminal ? (
<>
Domaine de l'instance VPS
{!isEditingDomain ? (
{details.domain}
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
) : (
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} />
{isUpdating ? 'SYNC...' : 'SAUVEGARDER'}
setIsEditingDomain(false)} className="bg-gray-800 text-white px-3 py-1.5 rounded text-xs hover:bg-gray-700">ANNULER
)}
{isCustomDomain && (
Domaine externe détecté. Sécurisez après pointage DNS.
{sslStatus === 'loading' ? 'GÉNÉRATION...' : <> GÉNÉRER SSL>}
)}
{/* ACCÈS DE FAUT REMIS À JOUR */}
Identifiants d'usine :
Console SSH : root / Clé choisie à l'initialisation
Explorateur : admin / admin
Explorateur de Fichiers
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">
Ouvrir l'explorateur
Console Système
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">
{">_"} Ouvrir le Terminal
>
) : (
NEXUS TERMINAL ({details.domain})
{/* OPTION AJOUTÉE : OUVRIR EN PLEIN ÉCRAN DANS UN NOUVEL ONGLET */}
{ window.open(terminalUrl, '_blank'); setShowTerminal(false); }} className="text-xs bg-cyan-600/30 text-cyan-400 hover:bg-cyan-600 hover:text-white border border-cyan-500/30 px-3 py-1 rounded transition flex items-center gap-1">
Plein Écran ↗
setShowTerminal(false)} className="text-xs bg-red-900/30 text-white px-3 py-1 rounded">Fermer
)}
);
};
// ============================================================================
// COMPOSANT 4 : WEB MANAGER (HestiaCP & Day-2 Operations)
// ============================================================================
const WebManager = ({ details, orderId, onRefresh, onOpenSso, onAlert }) => {
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();
onAlert("Succès", "Domaine web mis à jour avec succès sur le serveur HestiaCP !", "success");
} else onAlert("Erreur", data.error, "error");
} catch (err) {
onAlert("Erreur", "Erreur de connexion avec l'API.", "error");
} 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');
onAlert("SSL Validé", "Certificat SSL Let's Encrypt généré !", "success");
} else {
setSslStatus('error');
onAlert("Erreur DNS", "Échec Let's Encrypt. Vérifiez vos DNS.", "error");
}
} 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);
onOpenSso({ username: details.username, password: rollingPassword, url: 'https://panel.gise.be/login/' });
} catch (err) {
onAlert("Erreur Métal", "Erreur lors de la génération du SSO HestiaCP.", "error");
} finally {
setIsGeneratingSso(false);
}
};
return (
Domaine du Site Web
{!isEditingDomain ? (
{details.domain}
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
) : (
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} />
{isUpdating ? 'SYNC...' : 'SAUVEGARDER'}
setIsEditingDomain(false)} className="bg-gray-800 text-white px-3 py-1.5 rounded text-xs hover:bg-gray-700">ANNULER
)}
{isCustomDomain && (
Domaine personnalisé détecté. Sécurisez après pointage DNS.
{sslStatus === 'loading' ? 'GÉNÉRATION...' : <> GÉNÉRER SSL>}
)}
Visiter le Site
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">
Ouvrir dans le navigateur
Panel de Contrôle
{isGeneratingSso ? (
GÉNÉRATION SSO...
) : (
<>
Gérer l'hébergement
>
)}
);
};
// ============================================================================
// COMPOSANT PRINCIPAL : SERVICES
// ============================================================================
export default function Services() {
const [services, setServices] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [newVpcName, setNewVpcName] = useState("");
const [isConnecting, setIsConnecting] = useState(null);
const [ssoVault, setSsoVault] = useState(null);
const [activeServiceModal, setActiveServiceModal] = useState(null);
const [customAlert, setCustomAlert] = useState(null); // Gère les pop-ups personnalisés
const { vpcs, createVPC, deleteVPC, assignToVPC, removeFromVPC } = useVPC();
const triggerAlert = (title, message, type = "info") => {
setCustomAlert({ title, message, type });
};
const fetchServices = useCallback(async () => {
try {
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 };
})
);
const filteredServices = detailedServices.filter(s => {
const type = (s.type || '').toLowerCase();
const title = (s.title || '').toLowerCase();
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);
}
}, []);
useEffect(() => {
fetchServices();
}, [fetchServices]);
const handleOpenConsole = 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');
const isWeb = !isVPS && !isCloud && !isDB;
setIsConnecting(service.id);
try {
if (isCloud) {
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) {
window.open('https://pma.gise.be/', '_blank');
} else if (isVPS || isWeb) {
const freshDetails = await getHostingServiceDetails(service.id);
setActiveServiceModal({ type: isVPS ? 'vps' : 'web', data: service, details: freshDetails });
}
} catch (err) {
triggerAlert("Erreur d'Orchestration", "Échec du protocole : " + err.message, "error");
} finally {
setIsConnecting(null);
}
};
const assignedServiceIds = vpcs.flatMap(vpc => vpc.services);
const freeServices = services.filter(s => !assignedServiceIds.includes(s.id));
if (isLoading) return ANALYSE DU RÉSEAU...
;
return (
INVENTAIRE RÉSEAU
Orchestration des environnements et des Virtual Private Clouds.
{error &&
{error}
}
{vpcs.map(vpc => {
const vpcServices = services.filter(s => vpc.services.includes(s.id));
return (
{vpc.name.toUpperCase()}
{vpcServices.length} INSTANCES
{ deleteVPC(vpc.id); triggerAlert("VPC Démantelé", "Le groupe de routage a été dissous. Les instances ont été reversées dans le pool libre.", "info"); }} className="text-gray-500 hover:text-red-400 transition-colors flex items-center text-sm">
Démanteler VPC
{vpcServices.length === 0 ? (
Réseau virtuel vide. Assigner des instances depuis le pool libre.
) : (
vpcServices.map(service => (
))
)}
);
})}
POOL D'INSTANCES LIBRES
{freeServices.length === 0 ? (
Aucune instance libre. Toutes vos accréditations sont assignées à des VPC.
) : (
freeServices.map(service => (
))
)}
{/* MODAL 1 : HESTIACP SSO VAULT */}
{ssoVault && (
ACCÈS AUTORISÉ
{/* La croix permet de fermer manuellement le modal après copie */}
setSsoVault(null)} className="text-gray-400 hover:text-white transition text-lg">✖
Le pare-feu HestiaCP bloque les injections directes. Un mot de passe jetable a été généré. Copiez-le et connectez-vous.
Utilisateur
{ssoVault.username}
navigator.clipboard.writeText(ssoVault.username)} className="text-xs bg-gray-800 hover:bg-gray-700 text-white px-3 py-1 rounded transition">Copier
Clé Éphémère
{ssoVault.password}
navigator.clipboard.writeText(ssoVault.password)} className="text-xs bg-emerald-600 hover:bg-emerald-500 text-white px-3 py-1 rounded transition">Copier
{/* REFIXÉ : Pas de setSsoVault(null) ici pour garder le modal ouvert au retour de l'onglet */}
Ouvrir le Panel Web ↗
)}
{/* MODAL 2 : GESTIONNAIRE DE SERVICE GÉNÉRIQUE (VPS & WEB) */}
{activeServiceModal && (
{activeServiceModal.type === 'vps' ? (
<> {activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? "GESTION DE L'INSTANCE" : "PHASE D'INITIALISATION"}>
) : (
<> GESTION DE L'HÉBERGEMENT WEB>
)}
setActiveServiceModal(null)} className="text-gray-400 hover:text-white transition text-xl">✖
{activeServiceModal.type === 'vps' ? (
activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? (
) : (
{ setActiveServiceModal(null); fetchServices(); }} onAlert={triggerAlert} />
)
) : (
)}
)}
{/* VRAI MODAL DE NOTIFICATION REACT SURCHARGE */}
setCustomAlert(null)} />
);
}