remove alert
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 31s

This commit is contained in:
maximus
2026-06-22 22:27:39 +02:00
parent bdcb6cb3f9
commit 421b97156a
3 changed files with 224 additions and 82 deletions
+75 -8
View File
@@ -1,12 +1,75 @@
// src/layouts/AppLayout.jsx
import { useState } from 'react';
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
// ============================================================================
// COMPOSANT : MODAL DE CONFIRMATION DE DÉCONNEXION
// ============================================================================
const ConfirmLogoutModal = ({ isOpen, onClose, onConfirm }) => {
if (!isOpen) return null;
return (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.8)', backdropFilter: 'blur(4px)',
zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
<div style={{
backgroundColor: '#1A1A1A',
border: '1px solid #ff003c',
boxShadow: '0 10px 30px rgba(255, 0, 60, 0.2)',
borderRadius: '8px',
maxWidth: '400px',
width: '100%',
padding: '24px',
color: '#FFF',
fontFamily: 'monospace'
}}>
<h4 style={{ fontSize: '1.1rem', fontWeight: 'bold', letterSpacing: '1px', marginBottom: '10px', color: '#ff003c', textTransform: 'uppercase' }}>
DÉCONNEXION DU TERMINAL
</h4>
<p style={{ fontSize: '0.9rem', color: '#AAA', marginBottom: '24px', lineHeight: '1.5' }}>
Êtes-vous sûr de vouloir fermer la session sécurisée et quitter l'environnement cloud ?
</p>
<div style={{ display: 'flex', gap: '10px' }}>
<button onClick={onClose} style={{
flex: 1, padding: '10px',
backgroundColor: '#333', color: '#FFF',
border: 'none', borderRadius: '4px',
fontFamily: 'monospace', fontWeight: 'bold', cursor: 'pointer', letterSpacing: '1px'
}}>
ANNULER
</button>
<button onClick={onConfirm} style={{
flex: 1, padding: '10px',
backgroundColor: '#ff003c', color: '#FFF',
border: 'none', borderRadius: '4px',
fontFamily: 'monospace', fontWeight: 'bold', cursor: 'pointer', letterSpacing: '1px'
}}>
SE DÉCONNECTER
</button>
</div>
</div>
</div>
);
};
// ============================================================================
// COMPOSANT PRINCIPAL : LAYOUT DU PORTAIL
// ============================================================================
export default function AppLayout() {
const navigate = useNavigate();
const [showLogoutModal, setShowLogoutModal] = useState(false);
const handleLogout = () => {
// Ici, tu pourras ajouter la logique de déconnexion (effacer les cookies/tokens)
alert("[ DÉCONNEXION EN COURS... ]");
// Ouvre simplement le modal
const handleLogoutClick = () => {
setShowLogoutModal(true);
};
// Exécute la vraie déconnexion
const confirmLogout = () => {
// Ici, tu pourras effacer les tokens (ex: localStorage.removeItem('token'))
setShowLogoutModal(false);
navigate('/login');
};
@@ -83,7 +146,7 @@ export default function AppLayout() {
{/* Bas de la Sidebar (Déconnexion) */}
<div style={{ padding: '20px', borderTop: '1px solid #222' }}>
<button
onClick={handleLogout}
onClick={handleLogoutClick}
style={{
width: '100%', padding: '12px', backgroundColor: 'transparent',
color: '#ff003c', border: '1px solid #ff003c', cursor: 'pointer',
@@ -99,13 +162,17 @@ export default function AppLayout() {
{/* ZONE DE CONTENU DYNAMIQUE */}
{/* ========================================== */}
<main style={mainContentStyle}>
{/* Le composant <Outlet /> est magique :
C'est ici que React Router va injecter le contenu de la page demandée
(Dashboard, Settings, etc.) sans jamais recharger la barre latérale !
*/}
{/* Le composant <Outlet /> injecte le contenu de la page demandée sans recharger la sidebar */}
<Outlet />
</main>
{/* MODAL DE DÉCONNEXION */}
<ConfirmLogoutModal
isOpen={showLogoutModal}
onClose={() => setShowLogoutModal(false)}
onConfirm={confirmLogout}
/>
</div>
);
}
+72 -33
View File
@@ -1,7 +1,28 @@
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 } from 'lucide-react';
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 (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-[100] flex items-center justify-center p-4">
<div className={`bg-gray-900 border ${isError ? 'border-red-500/50 shadow-red-500/10' : 'border-cyan-500/50 shadow-cyan-500/10'} rounded-lg shadow-2xl max-w-sm w-full p-6 text-gray-200`}>
<h4 className={`text-lg font-mono font-bold tracking-wider mb-2 ${isError ? 'text-red-400' : 'text-cyan-400'}`}>
{notification.title.toUpperCase()}
</h4>
<p className="text-sm text-gray-400 mb-6">{notification.message}</p>
<button onClick={onClose} className={`w-full font-mono py-2 rounded text-xs font-bold transition ${isError ? 'bg-red-600 hover:bg-red-500 text-white' : 'bg-cyan-500 hover:bg-cyan-400 text-gray-950'}`}>
COMPRIS
</button>
</div>
</div>
);
};
// ============================================================================
// COMPOSANT 1 : INSTANCE CARD (Mode PaaS Pur - IP Masquée)
@@ -129,13 +150,13 @@ const InstanceCard = ({ service, vpcs, onAssignVpc, onRemoveVpc, onOpenConsole,
// ============================================================================
// COMPOSANT 2 : VPS DEPLOYER (Instanciation)
// ============================================================================
const VpsDeployer = ({ serviceId, onDeploySuccess }) => {
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) { alert("Le mot de passe doit faire au moins 8 caractères."); return; }
if (password.length < 8) { onAlert("Sécurité", "Le mot de passe doit faire au moins 8 caractères.", "error"); return; }
setIsDeploying(true);
try {
@@ -149,10 +170,10 @@ const VpsDeployer = ({ serviceId, onDeploySuccess }) => {
if (data.status === 'success') {
onDeploySuccess({ ip: data.ip, domain: data.domain, password: password });
} else {
alert("Erreur de provisionnement : " + (data.message || 'Inconnue'));
onAlert("Échec", "Erreur de provisionnement : " + (data.message || 'Inconnue'), "error");
}
} catch (err) {
alert("Erreur de communication avec l'API Proxmox Gateway.");
onAlert("Erreur Réseau", "Erreur de communication avec l'API Proxmox Gateway.", "error");
} finally {
setIsDeploying(false);
}
@@ -185,7 +206,7 @@ const VpsDeployer = ({ serviceId, onDeploySuccess }) => {
// ============================================================================
// COMPOSANT 3 : VPS MANAGER (Day-2 Operations)
// ============================================================================
const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
const VpsManager = ({ details, orderId, onRefresh, onAlert }) => {
const [showTerminal, setShowTerminal] = useState(false);
const [isEditingDomain, setIsEditingDomain] = useState(false);
const [newDomain, setNewDomain] = useState(details.domain);
@@ -211,10 +232,10 @@ const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
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);
onAlert("Succès", "Domaine VPS mis à jour avec succès sur l'infrastructure !", "success");
} else onAlert("Erreur", data.error, "error");
} catch (err) {
alert("Erreur de connexion avec l'API.");
onAlert("Erreur", "Erreur de connexion avec l'API.", "error");
} finally { setIsUpdating(false); }
};
@@ -229,10 +250,10 @@ const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
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 !");
onAlert("SSL Activé", "Certificat SSL Let's Encrypt généré et activé avec succès !", "success");
} else {
setSslStatus('error');
alert("Échec de la validation DNS. Vérifiez que votre domaine pointe bien vers notre IP.");
onAlert("Challenge DNS Échoué", "Vérifiez que votre domaine pointe bien vers notre IP publique.", "error");
}
} catch (e) { setSslStatus('error'); }
};
@@ -268,6 +289,13 @@ const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
</button>
</div>
)}
{/* ACCÈS DE FAUT REMIS À JOUR */}
<div className="mt-4 bg-black/60 border border-gray-900 rounded p-3 text-[11px] space-y-1">
<span className="text-gray-500 font-bold uppercase tracking-wider block mb-1 text-[10px]">Identifiants d'usine :</span>
<p className="text-gray-400 font-mono">Console SSH : <span className="text-cyan-400 font-bold">root</span> / <span className="text-gray-500 italic">Clé choisie à l'initialisation</span></p>
<p className="text-gray-400 font-mono">Explorateur : <span className="text-cyan-400 font-bold">admin</span> / <span className="text-cyan-400 font-bold">admin</span></p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
@@ -291,8 +319,14 @@ const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
<div>
<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">NEXUS TERMINAL ({details.domain})</span>
<div className="flex items-center space-x-2">
{/* OPTION AJOUTÉE : OUVRIR EN PLEIN ÉCRAN DANS UN NOUVEL ONGLET */}
<button onClick={() => { 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">
<Maximize2 className="w-3 h-3" /> Plein Écran
</button>
<button onClick={() => setShowTerminal(false)} className="text-xs bg-red-900/30 text-white px-3 py-1 rounded">Fermer</button>
</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" />
</div>
@@ -305,7 +339,7 @@ const VpsManager = ({ details, orderId, onRefresh, onClose }) => {
// ============================================================================
// COMPOSANT 4 : WEB MANAGER (HestiaCP & Day-2 Operations)
// ============================================================================
const WebManager = ({ details, orderId, onRefresh, onOpenSso }) => {
const WebManager = ({ details, orderId, onRefresh, onOpenSso, onAlert }) => {
const [isEditingDomain, setIsEditingDomain] = useState(false);
const [newDomain, setNewDomain] = useState(details.domain);
const [isUpdating, setIsUpdating] = useState(false);
@@ -328,10 +362,10 @@ const WebManager = ({ details, orderId, onRefresh, onOpenSso }) => {
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);
onAlert("Succès", "Domaine web mis à jour avec succès sur le serveur HestiaCP !", "success");
} else onAlert("Erreur", data.error, "error");
} catch (err) {
alert("Erreur de connexion avec l'API.");
onAlert("Erreur", "Erreur de connexion avec l'API.", "error");
} finally { setIsUpdating(false); }
};
@@ -346,10 +380,10 @@ const WebManager = ({ details, orderId, onRefresh, onOpenSso }) => {
const data = await response.json();
if (data.status === 'success') {
setSslStatus('success');
alert("Certificat SSL Let's Encrypt généré !");
onAlert("SSL Validé", "Certificat SSL Let's Encrypt généré !", "success");
} else {
setSslStatus('error');
alert("Échec Let's Encrypt. Vérifiez vos DNS.");
onAlert("Erreur DNS", "Échec Let's Encrypt. Vérifiez vos DNS.", "error");
}
} catch (e) { setSslStatus('error'); }
};
@@ -360,11 +394,9 @@ const WebManager = ({ details, orderId, onRefresh, onOpenSso }) => {
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.");
onAlert("Erreur Métal", "Erreur lors de la génération du SSO HestiaCP.", "error");
} finally {
setIsGeneratingSso(false);
}
@@ -438,11 +470,15 @@ export default function Services() {
const [newVpcName, setNewVpcName] = useState("");
const [isConnecting, setIsConnecting] = useState(null);
const [ssoVault, setSsoVault] = useState(null);
const [activeServiceModal, setActiveServiceModal] = useState(null); // Gère VPS et WEB
const [activeServiceModal, setActiveServiceModal] = useState(null);
const [customAlert, setCustomAlert] = useState(null); // Gère les pop-ups personnalisés
const { vpcs, createVPC, deleteVPC, assignToVPC, removeFromVPC } = useVPC();
// Fonction extraite pour pouvoir être rappelée par les Managers
const triggerAlert = (title, message, type = "info") => {
setCustomAlert({ title, message, type });
};
const fetchServices = useCallback(async () => {
try {
const data = await getMyServices();
@@ -495,12 +531,11 @@ export default function Services() {
} else if (isDB) {
window.open('https://pma.gise.be/', '_blank');
} else if (isVPS || isWeb) {
// Pour VPS et Web, on récupère les détails frais et on ouvre le Modal Générique
const freshDetails = await getHostingServiceDetails(service.id);
setActiveServiceModal({ type: isVPS ? 'vps' : 'web', data: service, details: freshDetails });
}
} catch (err) {
alert("Échec du protocole : " + err.message);
triggerAlert("Erreur d'Orchestration", "Échec du protocole : " + err.message, "error");
} finally {
setIsConnecting(null);
}
@@ -521,7 +556,7 @@ export default function Services() {
<div className="flex space-x-2 bg-gray-900 p-2 rounded-xl border border-gray-800">
<input type="text" placeholder="Nom du nouveau VPC..." value={newVpcName} onChange={(e) => setNewVpcName(e.target.value)} className="bg-black border border-gray-800 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:border-cyan-400 w-64" />
<button onClick={() => { createVPC(newVpcName); setNewVpcName(""); }} disabled={!newVpcName.trim()} className="bg-cyan-400 text-gray-900 px-4 py-2 rounded-lg font-bold text-sm disabled:opacity-50 hover:bg-cyan-300 flex items-center">
<button onClick={() => { createVPC(newVpcName); setNewVpcName(""); triggerAlert("VPC", "Nouveau groupe de routage VPC créé avec succès !", "success"); }} disabled={!newVpcName.trim()} className="bg-cyan-400 text-gray-900 px-4 py-2 rounded-lg font-bold text-sm disabled:opacity-50 hover:bg-cyan-300 flex items-center">
<Plus className="w-4 h-4 mr-1" /> CRÉER GROUPE
</button>
</div>
@@ -540,7 +575,7 @@ export default function Services() {
<h2 className="text-xl font-bold tracking-wider">{vpc.name.toUpperCase()}</h2>
<span className="bg-gray-800 text-gray-400 px-2 py-0.5 rounded text-xs font-mono">{vpcServices.length} INSTANCES</span>
</div>
<button onClick={() => deleteVPC(vpc.id)} className="text-gray-500 hover:text-red-400 transition-colors flex items-center text-sm">
<button onClick={() => { 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">
<Trash2 className="w-4 h-4 mr-1" /> Démanteler VPC
</button>
</div>
@@ -591,7 +626,8 @@ export default function Services() {
<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">
<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>
{/* La croix permet de fermer manuellement le modal après copie */}
<button onClick={() => setSsoVault(null)} className="text-gray-400 hover:text-white transition text-lg">✖</button>
</div>
<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">
@@ -610,8 +646,9 @@ export default function Services() {
</div>
</div>
</div>
<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 Panel Web
{/* REFIXÉ : Pas de setSsoVault(null) ici pour garder le modal ouvert au retour de l'onglet */}
<a href={ssoVault.url} target="_blank" rel="noopener noreferrer" 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 Panel Web
</a>
</div>
</div>
@@ -632,19 +669,21 @@ export default function Services() {
<button onClick={() => setActiveServiceModal(null)} className="text-gray-400 hover:text-white transition text-xl">✖</button>
</div>
{/* ROUTAGE DU COMPOSANT INTERNE SELON L'ÉTAT DU SERVICE */}
{activeServiceModal.type === 'vps' ? (
activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? (
<VpsManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} />
<VpsManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onAlert={triggerAlert} />
) : (
<VpsDeployer serviceId={activeServiceModal.data.id} onDeploySuccess={() => { setActiveServiceModal(null); fetchServices(); }} />
<VpsDeployer serviceId={activeServiceModal.data.id} onDeploySuccess={() => { setActiveServiceModal(null); fetchServices(); }} onAlert={triggerAlert} />
)
) : (
<WebManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onOpenSso={setSsoVault} />
<WebManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onOpenSso={setSsoVault} onAlert={triggerAlert} />
)}
</div>
</div>
)}
{/* VRAI MODAL DE NOTIFICATION REACT SURCHARGE */}
<NotificationModal notification={customAlert} onClose={() => setCustomAlert(null)} />
</div>
);
}
+76 -40
View File
@@ -1,8 +1,50 @@
// src/pages/public/Register.jsx
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { registerUnifiedClient } from '../../services/api';
// ============================================================================
// COMPOSANT : MODAL DE NOTIFICATION (Succès / Erreur)
// ============================================================================
const NotificationModal = ({ notification, onClose }) => {
if (!notification) return null;
const isError = notification.type === 'error';
return (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-[100] flex items-center justify-center p-4" style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.8)', backdropFilter: 'blur(4px)', zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{
backgroundColor: '#1A1A1A',
border: `1px solid ${isError ? '#ff003c' : '#00E5FF'}`,
boxShadow: `0 10px 30px ${isError ? 'rgba(255, 0, 60, 0.2)' : 'rgba(0, 229, 255, 0.2)'}`,
borderRadius: '8px',
maxWidth: '400px',
width: '100%',
padding: '24px',
color: '#FFF',
fontFamily: 'monospace'
}}>
<h4 style={{ fontSize: '1.1rem', fontWeight: 'bold', letterSpacing: '1px', marginBottom: '10px', color: isError ? '#ff003c' : '#00E5FF' }}>
{notification.title.toUpperCase()}
</h4>
<p style={{ fontSize: '0.9rem', color: '#AAA', marginBottom: '24px', whiteSpace: 'pre-line', lineHeight: '1.5' }}>
{notification.message}
</p>
<button onClick={onClose} style={{
width: '100%', padding: '10px',
backgroundColor: isError ? '#ff003c' : '#00E5FF',
color: isError ? '#FFF' : '#000',
border: 'none', borderRadius: '4px',
fontFamily: 'monospace', fontWeight: 'bold', cursor: 'pointer', letterSpacing: '1px'
}}>
COMPRIS
</button>
</div>
</div>
);
};
// ============================================================================
// COMPOSANT PRINCIPAL : REGISTER
// ============================================================================
export default function Register() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
@@ -10,60 +52,65 @@ export default function Register() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [customAlert, setCustomAlert] = useState(null); // Remplace l'état "error" et "alert()"
const navigate = useNavigate();
const handleRegister = async (e) => {
e.preventDefault();
setError(null);
setCustomAlert(null);
// 1. DÉFINITION DE LA POLITIQUE DE MOT DE PASSE (Le Checkpoint)
// Explication de la Regex :
// (?=.*[a-z]) : Au moins une minuscule
// (?=.*[A-Z]) : Au moins une majuscule
// (?=.*\d) : Au moins un chiffre
// (?=.*[\W_]) : Au moins un caractère spécial (non-alphanumérique ou underscore)
// .{8,} : Minimum 8 caractères au total
const passwordPolicy = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/;
// 2. VÉRIFICATION
// VÉRIFICATIONS FRONTEND (Affiche le Modal d'Erreur)
if (!passwordPolicy.test(password)) {
setError("ERREUR : Le mot de passe doit contenir 8 caractères min, une majuscule, une minuscule, un chiffre et un caractère spécial.");
return; // On stoppe l'exécution ici. La requête ne part pas vers le serveur.
}
// Sécurité Frontend : Validation des mots de passe avant envoi au serveur
if (password !== confirmPassword) {
setError("Les clés d'accès (mots de passe) ne correspondent pas.");
setCustomAlert({ type: 'error', title: 'Sécurité compromise', message: "Le mot de passe doit contenir 8 caractères min, une majuscule, une minuscule, un chiffre et un caractère spécial." });
return;
}
if (password !== confirmPassword) {
setCustomAlert({ type: 'error', title: 'Erreur de saisie', message: "Les clés d'accès (mots de passe) ne correspondent pas." });
return;
}
// Sécurité Frontend : Regex simple pour valider le format exigé par HestiaCP
const validUsername = /^[a-zA-Z0-9]{3,12}$/.test(username);
if (!validUsername) {
setError("Le nom d'utilisateur doit contenir uniquement des lettres ou chiffres (entre 3 et 12 caractères, sans espace).");
setCustomAlert({ type: 'error', title: 'Identifiant invalide', message: "Le nom d'utilisateur doit contenir uniquement des lettres ou chiffres (entre 3 et 12 caractères, sans espace)." });
return;
}
setLoading(true);
try {
// Envoi de la requête groupée à notre orchestrateur PHP backend
// API BACKEND
await registerUnifiedClient(email, username, password, firstName, lastName);
alert("[ PROVISIONNEMENT RÉUSSI ]\nVos comptes FOSSBilling, HestiaCP et Nextcloud ont été initialisés.\nVous pouvez maintenant vous connecter.");
// SUCCÈS (Affiche le Modal de Succès)
setCustomAlert({
type: 'success',
title: 'PROVISIONNEMENT RÉUSSI',
message: "Vos comptes FOSSBilling, HestiaCP et Nextcloud ont été initialisés.\n\nVous pouvez maintenant vous connecter à l'infrastructure."
});
// Redirection automatique vers la page de login après succès
navigate('/login');
} catch (err) {
setError(err.message || "Échec de l'initialisation de l'infrastructure.");
// ERREUR API (Affiche le Modal d'Erreur API)
setCustomAlert({ type: 'error', title: 'Échec du Déploiement', message: err.message || "Échec de l'initialisation de l'infrastructure." });
} finally {
setLoading(false);
}
};
// Réutilisation de ton Design System "Bunker"
// Fermeture du Modal : Redirige vers le Login SI c'était un succès
const handleCloseModal = () => {
if (customAlert?.type === 'success') {
navigate('/login');
} else {
setCustomAlert(null);
}
};
// Styles
const inputStyle = {
width: '100%', padding: '10px', marginBottom: '15px',
backgroundColor: '#1A1A1A', color: '#00E5FF',
@@ -91,20 +138,6 @@ export default function Register() {
[ INITIALISATION DU PROVISIONNEMENT TRIPLE EN CASCADE ]
</p>
{/* Affichage des alertes système */}
{error && (
<div style={{
color: '#ff003c', // Un rouge néon agressif pour les erreurs
border: '1px solid #ff003c',
backgroundColor: 'rgba(255, 0, 60, 0.1)',
padding: '10px',
marginBottom: '15px',
fontFamily: 'monospace'
}}>
[ ALERTE SYSTÈME ] : {error}
</div>
)}
<form onSubmit={handleRegister}>
<div style={{ display: 'flex', gap: '15px' }}>
<div style={{ flex: 1 }}>
@@ -140,6 +173,9 @@ export default function Register() {
</Link>
</div>
</div>
{/* Affichage du Modal par-dessus le formulaire */}
<NotificationModal notification={customAlert} onClose={handleCloseModal} />
</div>
);
}