upgrade design
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Folder, ExternalLink, Play, Maximize2, Lock } from 'lucide-react';
|
import { Folder, ExternalLink, Play, Maximize2, Lock, Copy, Check } from 'lucide-react';
|
||||||
|
|
||||||
export default function VpsManager({ details, orderId, onRefresh, onAlert }) {
|
export default function VpsManager({ details, orderId, onRefresh, onAlert }) {
|
||||||
const [showTerminal, setShowTerminal] = useState(false);
|
const [showTerminal, setShowTerminal] = useState(false);
|
||||||
@@ -7,11 +7,18 @@ export default function VpsManager({ details, orderId, onRefresh, onAlert }) {
|
|||||||
const [newDomain, setNewDomain] = useState(details.domain);
|
const [newDomain, setNewDomain] = useState(details.domain);
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [sslStatus, setSslStatus] = useState(null);
|
const [sslStatus, setSslStatus] = useState(null);
|
||||||
|
const [copiedField, setCopiedField] = useState(null);
|
||||||
|
|
||||||
const isInternal = details.domain && details.domain.endsWith('.gise.be');
|
const isInternal = details.domain && details.domain.endsWith('.gise.be');
|
||||||
const fileManagerUrl = isInternal ? `https://file-${details.domain}` : `http://${details.domain}:8080`;
|
const fileManagerUrl = isInternal ? `https://file-${details.domain}` : `http://${details.domain}:8080`;
|
||||||
const terminalUrl = isInternal ? `https://terminal-${details.domain}` : `http://${details.domain}:8081`;
|
const terminalUrl = isInternal ? `https://terminal-${details.domain}` : `http://${details.domain}:8081`;
|
||||||
|
|
||||||
|
const handleCopy = (text, field) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setCopiedField(field);
|
||||||
|
setTimeout(() => setCopiedField(null), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdateDomain = async () => {
|
const handleUpdateDomain = async () => {
|
||||||
if (!newDomain || newDomain === details.domain) return;
|
if (!newDomain || newDomain === details.domain) return;
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
@@ -62,7 +69,44 @@ export default function VpsManager({ details, orderId, onRefresh, onAlert }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
||||||
|
{/* ENCART DES IDENTIFIANTS */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
|
<div className="bg-black/60 border border-gray-800 rounded p-3">
|
||||||
|
<span className="text-gray-500 font-bold uppercase tracking-wider block mb-2 text-[10px] flex items-center gap-1">
|
||||||
|
<Folder className="w-3 h-3" /> Accès Explorateur
|
||||||
|
</span>
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<span className="text-xs font-mono text-gray-400">User: <strong className="text-cyan-400">root</strong></span>
|
||||||
|
<button onClick={() => handleCopy('root', 'fileUser')} className="text-gray-500 hover:text-cyan-400 p-1">
|
||||||
|
{copiedField === 'fileUser' ? <Check className="w-3 h-3 text-cyan-400" /> : <Copy className="w-3 h-3" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-xs font-mono text-gray-400">Pass: <strong className="text-cyan-400">NexusGise2026!</strong></span>
|
||||||
|
<button onClick={() => handleCopy('NexusGise2026!', 'filePass')} className="text-gray-500 hover:text-cyan-400 p-1">
|
||||||
|
{copiedField === 'filePass' ? <Check className="w-3 h-3 text-cyan-400" /> : <Copy className="w-3 h-3" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-black/60 border border-gray-800 rounded p-3">
|
||||||
|
<span className="text-gray-500 font-bold uppercase tracking-wider block mb-2 text-[10px] flex items-center gap-1">
|
||||||
|
<Play className="w-3 h-3" /> Accès Console / SSH
|
||||||
|
</span>
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<span className="text-xs font-mono text-gray-400">User: <strong className="text-cyan-400">root</strong></span>
|
||||||
|
<button onClick={() => handleCopy('root', 'sshUser')} className="text-gray-500 hover:text-cyan-400 p-1">
|
||||||
|
{copiedField === 'sshUser' ? <Check className="w-3 h-3 text-cyan-400" /> : <Copy className="w-3 h-3" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-xs font-mono text-gray-400">Pass: <span className="text-gray-600 italic">Défini à l'installation</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BOUTONS D'ACTIONS */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-2">
|
||||||
<button onClick={() => window.open(fileManagerUrl, '_blank')} className="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">
|
<button onClick={() => window.open(fileManagerUrl, '_blank')} className="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" /> Explorateur</span><ExternalLink className="w-4 h-4 text-gray-600 group-hover:text-cyan-400" />
|
<span className="font-mono text-gray-300 text-sm flex items-center gap-2"><Folder className="w-4 h-4 text-cyan-500" /> Explorateur</span><ExternalLink className="w-4 h-4 text-gray-600 group-hover:text-cyan-400" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Globe, ExternalLink, Lock, Loader } from 'lucide-react';
|
import { Globe, ExternalLink, Lock, Loader, Copy, Check } from 'lucide-react';
|
||||||
import { resetHostingPassword } from '../../services/api';
|
import { resetHostingPassword } from '../../services/api';
|
||||||
|
|
||||||
export default function WebManager({ details, orderId, onRefresh, onOpenSso, onAlert }) {
|
export default function WebManager({ details, orderId, onRefresh, onAlert }) {
|
||||||
const [isEditingDomain, setIsEditingDomain] = useState(false);
|
const [isEditingDomain, setIsEditingDomain] = useState(false);
|
||||||
const [newDomain, setNewDomain] = useState(details.domain);
|
const [newDomain, setNewDomain] = useState(details.domain);
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [sslStatus, setSslStatus] = useState(null);
|
const [sslStatus, setSslStatus] = useState(null);
|
||||||
|
|
||||||
|
// Nouveaux états pour gérer les accès localement
|
||||||
const [isGeneratingSso, setIsGeneratingSso] = useState(false);
|
const [isGeneratingSso, setIsGeneratingSso] = useState(false);
|
||||||
|
const [ssoData, setSsoData] = useState(null);
|
||||||
|
const [copiedField, setCopiedField] = useState(null);
|
||||||
|
|
||||||
const isCustomDomain = details.domain && !details.domain.endsWith('.gise.be');
|
const isCustomDomain = details.domain && !details.domain.endsWith('.gise.be');
|
||||||
const siteUrl = (isCustomDomain && sslStatus !== 'success') ? `http://${details.domain}` : `https://${details.domain}`;
|
const siteUrl = (isCustomDomain && sslStatus !== 'success') ? `http://${details.domain}` : `https://${details.domain}`;
|
||||||
|
|
||||||
|
// Fonction utilitaire pour copier
|
||||||
|
const handleCopy = (text, field) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setCopiedField(field);
|
||||||
|
setTimeout(() => setCopiedField(null), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdateDomain = async () => {
|
const handleUpdateDomain = async () => {
|
||||||
if (!newDomain || newDomain === details.domain) return;
|
if (!newDomain || newDomain === details.domain) return;
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
@@ -39,9 +50,12 @@ export default function WebManager({ details, orderId, onRefresh, onOpenSso, onA
|
|||||||
const secureHash = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-4).toUpperCase();
|
const secureHash = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-4).toUpperCase();
|
||||||
const rollingPassword = `Nx${secureHash}`;
|
const rollingPassword = `Nx${secureHash}`;
|
||||||
await resetHostingPassword(orderId, rollingPassword);
|
await resetHostingPassword(orderId, rollingPassword);
|
||||||
onOpenSso({ username: details.username, password: rollingPassword, url: 'https://panel.gise.be/login/' });
|
setSsoData({ 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"); }
|
} catch (err) {
|
||||||
finally { setIsGeneratingSso(false); }
|
onAlert("Erreur Métal", "Erreur lors de la génération du SSO HestiaCP.", "error");
|
||||||
|
} finally {
|
||||||
|
setIsGeneratingSso(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -50,7 +64,9 @@ export default function WebManager({ details, orderId, onRefresh, onOpenSso, onA
|
|||||||
<label className="block text-xs uppercase tracking-widest text-emerald-500 mb-2">Domaine du Site Web</label>
|
<label className="block text-xs uppercase tracking-widest text-emerald-500 mb-2">Domaine du Site Web</label>
|
||||||
{!isEditingDomain ? (
|
{!isEditingDomain ? (
|
||||||
<div className="flex justify-between items-center">
|
<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>
|
<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>
|
<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>
|
||||||
) : (
|
) : (
|
||||||
@@ -60,6 +76,7 @@ export default function WebManager({ details, orderId, onRefresh, onOpenSso, onA
|
|||||||
<button onClick={() => setIsEditingDomain(false)} className="bg-gray-800 text-white px-3 py-1.5 rounded text-xs hover:bg-gray-700">ANNULER</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 && (
|
{isCustomDomain && (
|
||||||
<div className="mt-4 border-t border-gray-900 pt-3 flex justify-between items-center">
|
<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é.</p>
|
<p className="text-[11px] text-gray-500">Domaine personnalisé détecté.</p>
|
||||||
@@ -69,13 +86,48 @@ export default function WebManager({ details, orderId, onRefresh, onOpenSso, onA
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
||||||
|
<div className="grid grid-cols-1 gap-6 mb-6">
|
||||||
<button onClick={() => window.open(siteUrl, '_blank')} className="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">
|
<button onClick={() => window.open(siteUrl, '_blank')} className="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 Site</span><ExternalLink className="w-4 h-4 text-gray-600 group-hover:text-emerald-400" />
|
<span className="font-mono text-gray-300 text-sm flex items-center gap-2"><Globe className="w-4 h-4 text-emerald-500" /> Ouvrir le Site</span><ExternalLink className="w-4 h-4 text-gray-600 group-hover:text-emerald-400" />
|
||||||
</button>
|
|
||||||
<button onClick={handleOpenPanel} disabled={isGeneratingSso} className="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" /> SSO...</span> : <><span className="font-mono text-emerald-400 text-sm flex items-center gap-2"><Lock className="w-4 h-4" /> Panel</span><ExternalLink className="w-4 h-4 text-emerald-600 group-hover:text-emerald-400" /></>}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* ZONE DES ACCÈS PANEL */}
|
||||||
|
{!ssoData ? (
|
||||||
|
<button onClick={handleOpenPanel} disabled={isGeneratingSso} className="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 DU MOT DE PASSE...</span>
|
||||||
|
) : (
|
||||||
|
<><span className="font-mono text-emerald-400 text-sm flex items-center gap-2"><Lock className="w-4 h-4" /> Obtenir les accès Panel</span><ExternalLink className="w-4 h-4 text-emerald-600 group-hover:text-emerald-400" /></>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="bg-emerald-900/10 border border-emerald-500/30 rounded p-4">
|
||||||
|
<p className="text-[11px] text-emerald-400/80 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||||
|
<Lock className="w-3 h-3" /> Accès Panel Généré
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2 mb-4">
|
||||||
|
<div className="flex justify-between items-center bg-black/50 border border-gray-800 rounded p-2">
|
||||||
|
<span className="text-gray-400 text-xs font-mono">User: <strong className="text-white ml-1">{ssoData.username}</strong></span>
|
||||||
|
<button onClick={() => handleCopy(ssoData.username, 'webUser')} className="text-emerald-400 hover:text-emerald-300 transition-colors p-1">
|
||||||
|
{copiedField === 'webUser' ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center bg-black/50 border border-gray-800 rounded p-2">
|
||||||
|
<span className="text-gray-400 text-xs font-mono">Pass: <strong className="text-emerald-400 ml-1">{ssoData.password}</strong></span>
|
||||||
|
<button onClick={() => handleCopy(ssoData.password, 'webPass')} className="text-emerald-400 hover:text-emerald-300 transition-colors p-1">
|
||||||
|
{copiedField === 'webPass' ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={() => window.open(ssoData.url, '_blank')} className="w-full bg-emerald-500 hover:bg-emerald-400 text-black font-bold font-mono tracking-widest uppercase py-2.5 rounded transition shadow-[0_0_15px_rgba(16,185,129,0.2)]">
|
||||||
|
Ouvrir le Panel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+39
-132
@@ -1,12 +1,8 @@
|
|||||||
// src/layouts/AppLayout.jsx
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
||||||
import { getClientTickets } from '../services/api';
|
import { getClientTickets } from '../services/api';
|
||||||
import ConfirmLogoutModal from '../components/ui/ConfirmLogoutModal';
|
import ConfirmLogoutModal from '../components/ui/ConfirmLogoutModal'; // Le modal extrait !
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// COMPOSANT PRINCIPAL : LAYOUT DU PORTAIL
|
|
||||||
// ============================================================================
|
|
||||||
export default function AppLayout() {
|
export default function AppLayout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
||||||
@@ -16,187 +12,98 @@ export default function AppLayout() {
|
|||||||
try {
|
try {
|
||||||
const data = await getClientTickets();
|
const data = await getClientTickets();
|
||||||
if (data && data.list) {
|
if (data && data.list) {
|
||||||
// On cherche les tickets qui ont le statut 'on_hold'
|
|
||||||
const unread = data.list.filter(t => t.status === 'on_hold' || t.unread).length;
|
const unread = data.list.filter(t => t.status === 'on_hold' || t.unread).length;
|
||||||
setUnreadCount(unread);
|
setUnreadCount(unread);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {}
|
||||||
// Erreur silencieuse : on ne pollue pas l'écran si le réseau saute
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkUnreadTickets(); // Premier check immédiat
|
checkUnreadTickets();
|
||||||
const intervalId = setInterval(checkUnreadTickets, 120000); // Check toutes les 2 minutes
|
const intervalId = setInterval(checkUnreadTickets, 120000);
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Ouvre simplement le modal
|
|
||||||
const handleLogoutClick = () => {
|
|
||||||
setShowLogoutModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Exécute la vraie déconnexion
|
|
||||||
const confirmLogout = () => {
|
const confirmLogout = () => {
|
||||||
// Ici, tu pourras effacer les tokens (ex: localStorage.removeItem('token'))
|
|
||||||
setShowLogoutModal(false);
|
setShowLogoutModal(false);
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- DESIGN SYSTEM DU BUNKER ---
|
// 🌟 La fonction magique Tailwind pour les liens du menu
|
||||||
const sidebarStyle = {
|
const navLinkClass = ({ isActive }) =>
|
||||||
width: '260px',
|
`block px-6 py-4 no-underline border-l-4 transition-all uppercase tracking-widest text-sm font-mono ${
|
||||||
backgroundColor: '#121212', // Gris blindage
|
isActive
|
||||||
borderRight: '1px solid #222222',
|
? 'text-cyan-400 bg-cyan-400/5 border-cyan-400'
|
||||||
height: '100vh',
|
: 'text-[#888] border-transparent hover:text-gray-300 hover:bg-white/5'
|
||||||
display: 'flex',
|
}`;
|
||||||
flexDirection: 'column',
|
|
||||||
position: 'fixed',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
fontFamily: 'monospace'
|
|
||||||
};
|
|
||||||
|
|
||||||
const mainContentStyle = {
|
|
||||||
marginLeft: '260px', // Laisse la place à la sidebar
|
|
||||||
minHeight: '100vh',
|
|
||||||
backgroundColor: '#050505', // Fond abyssal
|
|
||||||
color: '#E0E0E0',
|
|
||||||
padding: '40px'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fonction pour styliser le lien actif (en Cyan)
|
|
||||||
const getNavLinkStyle = ({ isActive }) => ({
|
|
||||||
display: 'block',
|
|
||||||
padding: '15px 25px',
|
|
||||||
color: isActive ? '#00E5FF' : '#888',
|
|
||||||
backgroundColor: isActive ? 'rgba(0, 229, 255, 0.05)' : 'transparent',
|
|
||||||
textDecoration: 'none',
|
|
||||||
borderLeft: isActive ? '4px solid #00E5FF' : '4px solid transparent',
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '1px',
|
|
||||||
fontSize: '0.9rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'block' }}>
|
<div className="flex min-h-screen bg-[#050505] text-[#E0E0E0] font-sans">
|
||||||
|
|
||||||
{/* ========================================== */}
|
{/* SIDEBAR */}
|
||||||
{/* BARRE LATÉRALE (SIDEBAR) */}
|
<aside className="w-[260px] bg-[#121212] border-r border-[#222222] h-screen flex flex-col fixed top-0 left-0">
|
||||||
{/* ========================================== */}
|
|
||||||
<aside style={sidebarStyle}>
|
|
||||||
|
|
||||||
{/* En-tête de la Sidebar (Logo / Marque) */}
|
<div className="px-6 py-8 border-b border-[#222]">
|
||||||
<div style={{ padding: '30px 25px', borderBottom: '1px solid #222' }}>
|
<h1 className="text-white m-0 text-3xl font-black tracking-widest flex items-baseline gap-2">
|
||||||
<h1 style={{
|
|
||||||
color: '#FFF',
|
|
||||||
margin: 0,
|
|
||||||
fontSize: '1.8rem',
|
|
||||||
letterSpacing: '3px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'baseline',
|
|
||||||
gap: '8px'
|
|
||||||
}}>
|
|
||||||
NEXUS
|
NEXUS
|
||||||
<span style={{
|
<span className="text-cyan-400 text-[10px] tracking-widest font-normal">by GISE</span>
|
||||||
color: '#00E5FF',
|
|
||||||
fontSize: '0.7rem',
|
|
||||||
letterSpacing: '1px',
|
|
||||||
fontWeight: 'normal',
|
|
||||||
}}>
|
|
||||||
by GISE
|
|
||||||
</span>
|
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ color: '#555', fontSize: '0.7rem', margin: '8px 0 0 0' }}>
|
<p className="text-[#555] text-[10px] font-mono tracking-wider mt-2 uppercase">
|
||||||
CONSOLE D'INFRASTRUCTURE v1.0
|
Console d'infrastructure v1.0
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Menu de Navigation */}
|
<nav className="flex-1 mt-6">
|
||||||
<nav style={{ flex: 1, marginTop: '20px' }}>
|
<div className="px-6 mb-2 text-[#444] text-[10px] font-bold font-mono tracking-widest">
|
||||||
<div style={{ padding: '0 25px', marginBottom: '10px', color: '#444', fontSize: '0.7rem', fontWeight: 'bold' }}>
|
|
||||||
// SUPERVISION
|
// SUPERVISION
|
||||||
</div>
|
</div>
|
||||||
<NavLink style={getNavLinkStyle} to="/dashboard">Tableau de bord</NavLink>
|
<NavLink className={navLinkClass} to="/dashboard">Tableau de bord</NavLink>
|
||||||
<NavLink style={getNavLinkStyle} to="/services">Inventaire Réseau</NavLink>
|
<NavLink className={navLinkClass} to="/services">Inventaire Réseau</NavLink>
|
||||||
|
|
||||||
<div style={{ padding: '0 25px', marginTop: '25px', marginBottom: '10px', color: '#444', fontSize: '0.7rem', fontWeight: 'bold' }}>
|
<div className="px-6 mt-8 mb-2 text-[#444] text-[10px] font-bold font-mono tracking-widest">
|
||||||
// GESTION
|
// GESTION
|
||||||
</div>
|
</div>
|
||||||
{/*<NavLink style={getNavLinkStyle} to="/billing" locked>Facturation</NavLink>*/}
|
<NavLink className={navLinkClass} to="/support">
|
||||||
<NavLink style={getNavLinkStyle} to="/support">
|
<div className="flex justify-between items-center w-full">
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
|
||||||
<span>Tickets Support</span>
|
<span>Tickets Support</span>
|
||||||
|
|
||||||
{/* LE BADGE DE NOTIFICATION */}
|
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<span style={{
|
<span className="bg-cyan-400 text-black text-[10px] font-bold px-2 py-0.5 rounded-full shadow-[0_0_10px_rgba(0,229,255,0.6)] animate-pulse">
|
||||||
backgroundColor: '#00E5FF',
|
|
||||||
color: '#000',
|
|
||||||
fontSize: '0.65rem',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
padding: '2px 8px',
|
|
||||||
borderRadius: '12px',
|
|
||||||
boxShadow: '0 0 10px rgba(0,229,255,0.6)'
|
|
||||||
}}>
|
|
||||||
{unreadCount} NEW
|
{unreadCount} NEW
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{/*<NavLink style={getNavLinkStyle} to="/settings">Profil & Sécurité</NavLink>*/}
|
|
||||||
{/* === MODULES VERROUILLÉS (WORK IN PROGRESS) === */}
|
|
||||||
<div style={{
|
|
||||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
||||||
padding: '15px 25px', color: '#444', borderLeft: '4px solid transparent',
|
|
||||||
textTransform: 'uppercase', letterSpacing: '1px', fontSize: '0.9rem',
|
|
||||||
cursor: 'not-allowed', userSelect: 'none'
|
|
||||||
}}>
|
|
||||||
<span>Facturation</span>
|
|
||||||
<span style={{ fontSize: '0.6rem', color: '#333', border: '1px solid #333', padding: '2px 5px', borderRadius: '3px', fontWeight: 'bold' }}>PROCHAINEMENT</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
<div className="flex justify-between items-center px-6 py-4 text-[#444] border-l-4 border-transparent uppercase tracking-widest text-sm font-mono cursor-not-allowed select-none">
|
||||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
<span>Facturation</span>
|
||||||
padding: '15px 25px', color: '#444', borderLeft: '4px solid transparent',
|
<span className="text-[9px] text-[#333] border border-[#333] px-1.5 py-0.5 rounded font-bold">WIP</span>
|
||||||
textTransform: 'uppercase', letterSpacing: '1px', fontSize: '0.9rem',
|
</div>
|
||||||
cursor: 'not-allowed', userSelect: 'none'
|
<div className="flex justify-between items-center px-6 py-4 text-[#444] border-l-4 border-transparent uppercase tracking-widest text-sm font-mono cursor-not-allowed select-none">
|
||||||
}}>
|
|
||||||
<span>Profil & Sécurité</span>
|
<span>Profil & Sécurité</span>
|
||||||
<span style={{ fontSize: '0.6rem', color: '#333', border: '1px solid #333', padding: '2px 5px', borderRadius: '3px', fontWeight: 'bold' }}>PROCHAINEMENT</span>
|
<span className="text-[9px] text-[#333] border border-[#333] px-1.5 py-0.5 rounded font-bold">WIP</span>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Bas de la Sidebar (Déconnexion) */}
|
<div className="p-5 border-t border-[#222]">
|
||||||
<div style={{ padding: '20px', borderTop: '1px solid #222' }}>
|
|
||||||
<button
|
<button
|
||||||
onClick={handleLogoutClick}
|
onClick={() => setShowLogoutModal(true)}
|
||||||
style={{
|
className="w-full p-3 bg-transparent text-[#ff003c] hover:bg-[#ff003c]/10 border border-[#ff003c] cursor-pointer font-mono text-sm tracking-widest uppercase transition-colors rounded-sm"
|
||||||
width: '100%', padding: '12px', backgroundColor: 'transparent',
|
|
||||||
color: '#ff003c', border: '1px solid #ff003c', cursor: 'pointer',
|
|
||||||
fontFamily: 'monospace', textTransform: 'uppercase'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
[ Déconnexion ]
|
[ Déconnexion ]
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* ========================================== */}
|
{/* ZONE DE CONTENU */}
|
||||||
{/* ZONE DE CONTENU DYNAMIQUE */}
|
<main className="ml-[260px] p-10 flex-1 min-h-screen">
|
||||||
{/* ========================================== */}
|
|
||||||
<main style={mainContentStyle}>
|
|
||||||
{/* Le composant <Outlet /> injecte le contenu de la page demandée sans recharger la sidebar */}
|
|
||||||
<Outlet context={{ refreshNavbarTickets: checkUnreadTickets }} />
|
<Outlet context={{ refreshNavbarTickets: checkUnreadTickets }} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* MODAL DE DÉCONNEXION */}
|
|
||||||
<ConfirmLogoutModal
|
<ConfirmLogoutModal
|
||||||
isOpen={showLogoutModal}
|
isOpen={showLogoutModal}
|
||||||
onClose={() => setShowLogoutModal(false)}
|
onClose={() => setShowLogoutModal(false)}
|
||||||
onConfirm={confirmLogout}
|
onConfirm={confirmLogout}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,35 +1,28 @@
|
|||||||
// src/layouts/PublicLayout.jsx
|
|
||||||
import { Outlet, Link } from 'react-router-dom';
|
import { Outlet, Link } from 'react-router-dom';
|
||||||
|
|
||||||
export default function PublicLayout() {
|
export default function PublicLayout() {
|
||||||
return (
|
return (
|
||||||
<div style={{ backgroundColor: '#121212', color: '#E0E0E0', minHeight: '100vh', fontFamily: 'monospace' }}>
|
<div className="bg-[#121212] text-[#E0E0E0] min-h-screen font-mono">
|
||||||
<nav style={{ padding: '20px', borderBottom: '1px solid #242424', display: 'flex', gap: '15px', alignItems: 'center' }}>
|
<nav className="p-5 border-b border-[#242424] flex gap-4 items-center">
|
||||||
<Link to="/" style={{ display: 'flex', alignItems: 'baseline', gap: '6px', textDecoration: 'none' }}>
|
<Link to="/" className="flex items-baseline gap-1.5 no-underline">
|
||||||
<span style={{ color: '#FFF', fontSize: '1.2rem', letterSpacing: '2px', fontWeight: 'bold' }}>NEXUS</span>
|
<span className="text-white text-xl tracking-[2px] font-bold">NEXUS</span>
|
||||||
<span style={{ color: '#00E5FF', fontSize: '0.65rem', letterSpacing: '1px', fontWeight: 'normal'}}>by GISE</span>
|
<span className="text-cyan-400 text-[10px] tracking-[1px]">by GISE</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/offres" style={{ color: '#E0E0E0', textDecoration: 'none', marginLeft: '20px' }}>Catalogue</Link>
|
|
||||||
|
|
||||||
{/* AJOUT DU BOUTON D'INSCRIPTION */}
|
<Link to="/offres" className="text-[#E0E0E0] no-underline ml-5 hover:text-cyan-400 transition-colors">
|
||||||
<Link to="/register" style={{ color: '#A0A0A0', textDecoration: 'none', marginLeft: 'auto', fontSize: '0.9rem' }}>
|
Catalogue
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link to="/register" className="text-[#A0A0A0] no-underline ml-auto text-sm hover:text-white transition-colors">
|
||||||
Créer un compte
|
Créer un compte
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link to="/login" style={{
|
<Link to="/login" className="text-black bg-cyan-400 hover:bg-cyan-300 transition-colors px-3 py-1.5 no-underline font-bold text-sm">
|
||||||
color: '#000',
|
|
||||||
backgroundColor: '#00E5FF',
|
|
||||||
padding: '6px 12px',
|
|
||||||
textDecoration: 'none',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: '0.9rem'
|
|
||||||
}}>
|
|
||||||
Connexion
|
Connexion
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* C'est ici que les pages (Accueil, Login, etc.) vont s'afficher */}
|
<main className="p-5">
|
||||||
<main style={{ padding: '20px' }}>
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export default function Services() {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [newVpcName, setNewVpcName] = useState("");
|
const [newVpcName, setNewVpcName] = useState("");
|
||||||
const [isConnecting, setIsConnecting] = useState(null);
|
const [isConnecting, setIsConnecting] = useState(null);
|
||||||
const [ssoVault, setSsoVault] = useState(null);
|
|
||||||
const [activeServiceModal, setActiveServiceModal] = useState(null);
|
const [activeServiceModal, setActiveServiceModal] = useState(null);
|
||||||
const [customAlert, setCustomAlert] = useState(null);
|
const [customAlert, setCustomAlert] = useState(null);
|
||||||
|
|
||||||
@@ -125,16 +124,6 @@ export default function Services() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* MODAUX */}
|
|
||||||
{ssoVault && ( /* ... Gérer le ssoVault ici, ou l'extraire aussi si tu le souhaites ... */
|
|
||||||
<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-emerald-500/50 rounded-lg shadow-2xl shadow-emerald-500/20 max-w-md w-full p-6 text-gray-200">
|
|
||||||
<h3 className="text-xl font-bold text-emerald-400 font-mono tracking-wider mb-4">ACCÈS AUTORISÉ</h3>
|
|
||||||
<p className="text-sm text-gray-400 mb-6">Utilisateur : {ssoVault.username}<br/>Clé : {ssoVault.password}</p>
|
|
||||||
<button onClick={() => setSsoVault(null)}>Fermer</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeServiceModal && (
|
{activeServiceModal && (
|
||||||
<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">
|
||||||
@@ -148,7 +137,7 @@ export default function Services() {
|
|||||||
{activeServiceModal.type === 'vps' ? (
|
{activeServiceModal.type === 'vps' ? (
|
||||||
activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? <VpsManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onAlert={triggerAlert} /> : <VpsDeployer serviceId={activeServiceModal.data.id} onDeploySuccess={() => { setActiveServiceModal(null); fetchServices(); }} onAlert={triggerAlert} />
|
activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? <VpsManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onAlert={triggerAlert} /> : <VpsDeployer serviceId={activeServiceModal.data.id} onDeploySuccess={() => { setActiveServiceModal(null); fetchServices(); }} onAlert={triggerAlert} />
|
||||||
) : (
|
) : (
|
||||||
<WebManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onOpenSso={setSsoVault} onAlert={triggerAlert} />
|
<WebManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onAlert={triggerAlert} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+10
-50
@@ -2,68 +2,28 @@ import { Link } from 'react-router-dom';
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div style={{ textAlign: 'center', marginTop: '12vh' }}>
|
<div className="text-center mt-[12vh]">
|
||||||
|
<h1 className="text-white text-5xl md:text-6xl tracking-[6px] flex items-baseline justify-center gap-3 mb-5 font-black">
|
||||||
<h1 style={{
|
|
||||||
color: '#FFF',
|
|
||||||
fontSize: '4rem',
|
|
||||||
letterSpacing: '6px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'baseline',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: '12px',
|
|
||||||
margin: '0 0 20px 0'
|
|
||||||
}}>
|
|
||||||
NEXUS
|
NEXUS
|
||||||
<span style={{
|
<span className="text-cyan-400 text-xl md:text-2xl tracking-[2px] font-normal">
|
||||||
color: '#00E5FF',
|
|
||||||
fontSize: '1.4rem',
|
|
||||||
letterSpacing: '2px',
|
|
||||||
fontWeight: 'normal'
|
|
||||||
}}>
|
|
||||||
by GISE
|
by GISE
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h2 style={{
|
<h2 className="text-white text-xl md:text-2xl tracking-[2px] uppercase mb-5 font-normal">
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: '1.5rem',
|
|
||||||
letterSpacing: '2px',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
marginBottom: '20px',
|
|
||||||
fontWeight: 'normal'
|
|
||||||
}}>
|
|
||||||
Bienvenue dans l'infrastructure
|
Bienvenue dans l'infrastructure
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p style={{
|
<p className="text-[#A0A0A0] text-lg max-w-[600px] mx-auto leading-relaxed">
|
||||||
color: '#A0A0A0',
|
|
||||||
fontSize: '1.2rem',
|
|
||||||
maxWidth: '600px',
|
|
||||||
margin: '0 auto',
|
|
||||||
lineHeight: '1.6'
|
|
||||||
}}>
|
|
||||||
Hébergement web, instances VPS et Stockage Cloud en Belgique.
|
Hébergement web, instances VPS et Stockage Cloud en Belgique.
|
||||||
<br />Propulsé par une architecture bare-metal locale.
|
<br />Propulsé par une architecture bare-metal locale.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* LE BOUTON D'ACTION PRINCIPAL REDIRIGE VERS /REGISTER */}
|
<div className="mt-12">
|
||||||
<div style={{ marginTop: '50px' }}>
|
<Link
|
||||||
<Link to="/register" style={{
|
to="/register"
|
||||||
display: 'inline-block',
|
className="inline-block bg-transparent text-cyan-400 border border-cyan-400 px-6 py-3 font-mono text-base uppercase tracking-[1px] font-bold transition-all duration-300 shadow-[0_0_15px_rgba(0,229,255,0.1)] hover:bg-cyan-400/10 hover:shadow-[0_0_20px_rgba(0,229,255,0.3)] no-underline rounded-sm"
|
||||||
backgroundColor: 'transparent',
|
>
|
||||||
color: '#00E5FF',
|
|
||||||
border: '1px solid #00E5FF',
|
|
||||||
padding: '12px 24px',
|
|
||||||
fontSize: '1rem',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
textDecoration: 'none',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '1px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
transition: 'all 0.3s ease',
|
|
||||||
boxShadow: '0 0 15px rgba(0, 229, 255, 0.1)',
|
|
||||||
}}>
|
|
||||||
Démarrer le déploiement
|
Démarrer le déploiement
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+19
-49
@@ -15,23 +15,9 @@ export default function Login() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. APPEL À TON API BACKEND (FOSSBilling / PHP)
|
|
||||||
// Ici, tu mettras ton vrai 'fetch' vers ton serveur pour vérifier le mot de passe.
|
|
||||||
// Pour l'instant, on simule un délai réseau d'une seconde.
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
// --- SIMULATION D'AUTHENTIFICATION ---
|
|
||||||
// (À remplacer par la vraie validation de ton serveur)
|
|
||||||
await loginClient(email, password);
|
await loginClient(email, password);
|
||||||
|
|
||||||
// 2. LA CLÉ DU PROBLÈME EST ICI : L'ATTRIBUTION DU BADGE
|
|
||||||
// On sauvegarde le token (généralement renvoyé par ton API) dans le navigateur
|
|
||||||
localStorage.setItem('gise_token', 'secure_token_alphanumerique_factice');
|
localStorage.setItem('gise_token', 'secure_token_alphanumerique_factice');
|
||||||
|
|
||||||
// 3. AUTORISATION ET REDIRECTION
|
|
||||||
// Maintenant que le token est en poche, ProtectedRoute nous laissera passer !
|
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -39,72 +25,56 @@ export default function Login() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- DESIGN SYSTEM "BUNKER" ---
|
|
||||||
const inputStyle = {
|
|
||||||
width: '100%', padding: '10px', marginBottom: '20px',
|
|
||||||
backgroundColor: '#1A1A1A', color: '#00E5FF',
|
|
||||||
border: '1px solid #333', fontFamily: 'monospace', outline: 'none',
|
|
||||||
boxSizing: 'border-box'
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonStyle = {
|
|
||||||
width: '100%', padding: '12px', backgroundColor: loading ? '#333' : '#00E5FF',
|
|
||||||
color: loading ? '#888' : '#000', border: 'none', cursor: loading ? 'not-allowed' : 'pointer',
|
|
||||||
fontFamily: 'monospace', fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: '1px',
|
|
||||||
marginTop: '10px'
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '10vh', padding: '0 20px' }}>
|
<div className="flex justify-center mt-[10vh] px-5">
|
||||||
<div style={{
|
<div className="w-full max-w-[400px] bg-[#242424] p-8 border-t-4 border-cyan-400 shadow-[0_10px_30px_rgba(0,0,0,0.5)]">
|
||||||
width: '100%', maxWidth: '400px', backgroundColor: '#242424',
|
<h2 className="text-white uppercase mb-1 text-2xl text-center font-sans font-bold">
|
||||||
padding: '30px', borderTop: '4px solid #00E5FF', boxShadow: '0 10px 30px rgba(0,0,0,0.5)'
|
|
||||||
}}>
|
|
||||||
<h2 style={{ color: '#FFF', textTransform: 'uppercase', marginBottom: '5px', fontSize: '1.5rem', textAlign: 'center' }}>
|
|
||||||
Connexion au Nexus
|
Connexion au Nexus
|
||||||
</h2>
|
</h2>
|
||||||
<p style={{ color: '#888', fontFamily: 'monospace', fontSize: '0.85rem', marginBottom: '25px', textAlign: 'center' }}>
|
<p className="text-[#888] font-mono text-sm mb-6 text-center">
|
||||||
[ IDENTIFICATION REQUISE ]
|
[ IDENTIFICATION REQUISE ]
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div style={{
|
<div className="text-[#ff003c] border border-[#ff003c] bg-[#ff003c]/10 p-3 mb-5 font-mono text-sm">
|
||||||
color: '#ff003c', border: '1px solid #ff003c', backgroundColor: 'rgba(255, 0, 60, 0.1)',
|
|
||||||
padding: '10px', marginBottom: '20px', fontFamily: 'monospace', fontSize: '0.85rem'
|
|
||||||
}}>
|
|
||||||
[ ALERTE ] : {error}
|
[ ALERTE ] : {error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleLogin}>
|
<form onSubmit={handleLogin} className="font-mono">
|
||||||
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>IDENTIFIANT (E-MAIL)</label>
|
<label className="block text-[#888] mb-1 text-xs tracking-widest uppercase">Identifiant (E-mail)</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
style={inputStyle}
|
|
||||||
required
|
required
|
||||||
placeholder="admin@gise.be"
|
placeholder="admin@gise.be"
|
||||||
|
className="w-full p-3 mb-5 bg-[#1A1A1A] text-cyan-400 border border-[#333] outline-none focus:border-cyan-400 transition-colors"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>CLÉ D'ACCÈS SÉCURISÉE</label>
|
<label className="block text-[#888] mb-1 text-xs tracking-widest uppercase">Clé d'accès sécurisée</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
style={inputStyle}
|
|
||||||
required
|
required
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
|
className="w-full p-3 mb-5 bg-[#1A1A1A] text-cyan-400 border border-[#333] outline-none focus:border-cyan-400 transition-colors"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button type="submit" style={buttonStyle} disabled={loading}>
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full p-3 mt-2 font-bold uppercase tracking-widest transition-colors disabled:bg-[#333] disabled:text-[#888] disabled:cursor-not-allowed bg-cyan-400 hover:bg-cyan-300 text-black shadow-[0_0_15px_rgba(0,229,255,0.2)]"
|
||||||
|
>
|
||||||
{loading ? 'VÉRIFICATION...' : 'OUVRIR LE SAS'}
|
{loading ? 'VÉRIFICATION...' : 'OUVRIR LE SAS'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div style={{ marginTop: '20px', textAlign: 'center', fontSize: '0.85rem' }}>
|
<div className="mt-5 text-center text-sm font-mono text-[#888]">
|
||||||
<Link to="/register" style={{ color: '#888', textDecoration: 'none' }}>
|
Aucun accès réseau ?{' '}
|
||||||
Aucun accès réseau ? <span style={{ color: '#00E5FF' }}>S'enregistrer ></span>
|
<Link to="/register" className="text-cyan-400 hover:text-cyan-300 transition-colors no-underline">
|
||||||
|
S'enregistrer >
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ import { useNavigate, Link } from 'react-router-dom';
|
|||||||
import { registerUnifiedClient } from '../../services/api';
|
import { registerUnifiedClient } from '../../services/api';
|
||||||
import NotificationModal from '../../components/ui/NotificationModal';
|
import NotificationModal from '../../components/ui/NotificationModal';
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// COMPOSANT PRINCIPAL : REGISTER
|
|
||||||
// ============================================================================
|
|
||||||
export default function Register() {
|
export default function Register() {
|
||||||
const [firstName, setFirstName] = useState('');
|
const [firstName, setFirstName] = useState('');
|
||||||
const [lastName, setLastName] = useState('');
|
const [lastName, setLastName] = useState('');
|
||||||
@@ -15,7 +12,7 @@ export default function Register() {
|
|||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [customAlert, setCustomAlert] = useState(null); // Remplace l'état "error" et "alert()"
|
const [customAlert, setCustomAlert] = useState(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleRegister = async (e) => {
|
const handleRegister = async (e) => {
|
||||||
@@ -23,119 +20,81 @@ export default function Register() {
|
|||||||
setCustomAlert(null);
|
setCustomAlert(null);
|
||||||
|
|
||||||
const passwordPolicy = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/;
|
const passwordPolicy = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/;
|
||||||
|
|
||||||
// VÉRIFICATIONS FRONTEND (Affiche le Modal d'Erreur)
|
|
||||||
if (!passwordPolicy.test(password)) {
|
if (!passwordPolicy.test(password)) {
|
||||||
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." });
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
setCustomAlert({ type: 'error', title: 'Erreur de saisie', message: "Les clés d'accès (mots de passe) ne correspondent pas." });
|
setCustomAlert({ type: 'error', title: 'Erreur de saisie', message: "Les clés d'accès ne correspondent pas." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!/^[a-zA-Z0-9]{3,12}$/.test(username)) {
|
||||||
const validUsername = /^[a-zA-Z0-9]{3,12}$/.test(username);
|
setCustomAlert({ type: 'error', title: 'Identifiant invalide', message: "Le nom d'utilisateur doit contenir uniquement des lettres ou chiffres (entre 3 et 12 caractères)." });
|
||||||
if (!validUsername) {
|
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// API BACKEND
|
|
||||||
await registerUnifiedClient(email, username, password, firstName, lastName);
|
await registerUnifiedClient(email, username, password, firstName, lastName);
|
||||||
|
setCustomAlert({ type: 'success', title: 'PROVISIONNEMENT RÉUSSI', message: "Vos comptes FOSSBilling, HestiaCP et Nextcloud ont été initialisés.\n\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."
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ERREUR API (Affiche le Modal d'Erreur API)
|
setCustomAlert({ type: 'error', title: 'Échec du Déploiement', message: err.message || "Échec de l'initialisation." });
|
||||||
setCustomAlert({ type: 'error', title: 'Échec du Déploiement', message: err.message || "Échec de l'initialisation de l'infrastructure." });
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fermeture du Modal : Redirige vers le Login SI c'était un succès
|
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
if (customAlert?.type === 'success') {
|
if (customAlert?.type === 'success') navigate('/login');
|
||||||
navigate('/login');
|
else setCustomAlert(null);
|
||||||
} else {
|
|
||||||
setCustomAlert(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Styles
|
|
||||||
const inputStyle = {
|
|
||||||
width: '100%', padding: '10px', marginBottom: '15px',
|
|
||||||
backgroundColor: '#1A1A1A', color: '#00E5FF',
|
|
||||||
border: '1px solid #333', fontFamily: 'monospace', outline: 'none',
|
|
||||||
boxSizing: 'border-box'
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonStyle = {
|
|
||||||
width: '100%', padding: '12px', backgroundColor: loading ? '#333' : '#00E5FF',
|
|
||||||
color: loading ? '#888' : '#000', border: 'none', cursor: loading ? 'not-allowed' : 'pointer',
|
|
||||||
fontFamily: 'monospace', fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: '1px',
|
|
||||||
marginTop: '10px'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '5vh', padding: '0 20px' }}>
|
<div className="flex justify-center mt-[5vh] px-5">
|
||||||
<div style={{
|
<div className="w-full max-w-[500px] bg-[#242424] p-8 border-t-4 border-cyan-400 shadow-[0_10px_30px_rgba(0,0,0,0.5)]">
|
||||||
width: '100%', maxWidth: '500px', backgroundColor: '#242424',
|
<h2 className="text-white uppercase mb-1 text-2xl font-sans font-bold">
|
||||||
padding: '30px', borderTop: '4px solid #00E5FF', boxShadow: '0 10px 30px rgba(0,0,0,0.5)'
|
|
||||||
}}>
|
|
||||||
<h2 style={{ color: '#FFF', textTransform: 'uppercase', marginBottom: '5px', fontSize: '1.5rem' }}>
|
|
||||||
Créer un accès réseau
|
Créer un accès réseau
|
||||||
</h2>
|
</h2>
|
||||||
<p style={{ color: '#888', fontFamily: 'monospace', fontSize: '0.85rem', marginBottom: '25px' }}>
|
<p className="text-[#888] font-mono text-sm mb-6">
|
||||||
[ INITIALISATION DU PROVISIONNEMENT TRIPLE EN CASCADE ]
|
[ INITIALISATION DU PROVISIONNEMENT TRIPLE ]
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form onSubmit={handleRegister}>
|
<form onSubmit={handleRegister} className="font-mono">
|
||||||
<div style={{ display: 'flex', gap: '15px' }}>
|
<div className="flex gap-4">
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>PRÉNOM</label>
|
<label className="block text-[#888] mb-1 text-xs tracking-widest uppercase">Prénom</label>
|
||||||
<input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} style={inputStyle} required placeholder="John" />
|
<input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} required placeholder="John" className="w-full p-3 mb-4 bg-[#1A1A1A] text-cyan-400 border border-[#333] outline-none focus:border-cyan-400 transition-colors" />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>NOM</label>
|
<label className="block text-[#888] mb-1 text-xs tracking-widest uppercase">Nom</label>
|
||||||
<input type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} style={inputStyle} required placeholder="Doe" />
|
<input type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} required placeholder="Doe" className="w-full p-3 mb-4 bg-[#1A1A1A] text-cyan-400 border border-[#333] outline-none focus:border-cyan-400 transition-colors" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>ADRESSE E-MAIL (IDENTIFIANT FACTURATION)</label>
|
<label className="block text-[#888] mb-1 text-xs tracking-widest uppercase">E-mail (Facturation)</label>
|
||||||
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} style={inputStyle} required placeholder="john.doe@gise.be" />
|
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required placeholder="john.doe@gise.be" className="w-full p-3 mb-4 bg-[#1A1A1A] text-cyan-400 border border-[#333] outline-none focus:border-cyan-400 transition-colors" />
|
||||||
|
|
||||||
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>NOM D'UTILISATEUR SÉCURISÉ (ACCÈS HESTIA & CLOUD)</label>
|
<label className="block text-[#888] mb-1 text-xs tracking-widest uppercase">Identifiant sécurisé (Cloud)</label>
|
||||||
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} style={inputStyle} required placeholder="ex: jdoe42" />
|
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} required placeholder="ex: jdoe42" className="w-full p-3 mb-4 bg-[#1A1A1A] text-cyan-400 border border-[#333] outline-none focus:border-cyan-400 transition-colors" />
|
||||||
|
|
||||||
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>CLÉ D'ACCÈS (MOT DE PASSE UNIQUE)</label>
|
<label className="block text-[#888] mb-1 text-xs tracking-widest uppercase">Clé d'accès unique</label>
|
||||||
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} style={inputStyle} required placeholder="••••••••" />
|
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required placeholder="••••••••" className="w-full p-3 mb-4 bg-[#1A1A1A] text-cyan-400 border border-[#333] outline-none focus:border-cyan-400 transition-colors" />
|
||||||
|
|
||||||
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>CONFIRMER LA CLÉ D'ACCÈS</label>
|
<label className="block text-[#888] mb-1 text-xs tracking-widest uppercase">Confirmer la clé</label>
|
||||||
<input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} style={inputStyle} required placeholder="••••••••" />
|
<input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} required placeholder="••••••••" className="w-full p-3 mb-5 bg-[#1A1A1A] text-cyan-400 border border-[#333] outline-none focus:border-cyan-400 transition-colors" />
|
||||||
|
|
||||||
<button type="submit" style={buttonStyle} disabled={loading}>
|
<button type="submit" disabled={loading} className="w-full p-3 mt-2 font-bold uppercase tracking-widest transition-colors disabled:bg-[#333] disabled:text-[#888] disabled:cursor-not-allowed bg-cyan-400 hover:bg-cyan-300 text-black shadow-[0_0_15px_rgba(0,229,255,0.2)]">
|
||||||
{loading ? 'DÉPLOIEMENT EN COURS...' : 'LANCER LE PROVISIONNEMENT'}
|
{loading ? 'DÉPLOIEMENT EN COURS...' : 'LANCER LE PROVISIONNEMENT'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div style={{ marginTop: '20px', textAlign: 'center', fontSize: '0.85rem' }}>
|
<div className="mt-5 text-center text-sm font-mono text-[#888]">
|
||||||
<Link to="/login" style={{ color: '#888', textDecoration: 'none' }}>
|
Déjà enregistré ?{' '}
|
||||||
Déjà enregistré ? <span style={{ color: '#00E5FF' }}>Se connecter au terminal ></span>
|
<Link to="/login" className="text-cyan-400 hover:text-cyan-300 transition-colors no-underline">
|
||||||
|
Se connecter au terminal >
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Affichage du Modal par-dessus le formulaire */}
|
|
||||||
<NotificationModal notification={customAlert} onClose={handleCloseModal} />
|
<NotificationModal notification={customAlert} onClose={handleCloseModal} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { loginClient } from '../../services/api';
|
|
||||||
|
|
||||||
export default function Login() {
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleLogin = async (e) => {
|
|
||||||
e.preventDefault(); // Empêche la page de se rafraîchir
|
|
||||||
setError(null);
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// On envoie la requête à FOSSBilling
|
|
||||||
await loginClient(email, password);
|
|
||||||
|
|
||||||
// Si on arrive ici, le login est un succès !
|
|
||||||
// On redirige vers l'espace client sécurisé
|
|
||||||
navigate('/dashboard');
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message || "Accès refusé. Vérifiez vos identifiants.");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Styles CSS en variables pour garder le code lisible
|
|
||||||
const inputStyle = {
|
|
||||||
width: '100%', padding: '12px', marginBottom: '15px',
|
|
||||||
backgroundColor: '#1A1A1A', color: '#00E5FF',
|
|
||||||
border: '1px solid #333', fontFamily: 'monospace', outline: 'none'
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonStyle = {
|
|
||||||
width: '100%', padding: '12px', backgroundColor: loading ? '#333' : '#00E5FF',
|
|
||||||
color: loading ? '#888' : '#000', border: 'none', cursor: loading ? 'not-allowed' : 'pointer',
|
|
||||||
fontFamily: 'monospace', fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: '1px'
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '10vh' }}>
|
|
||||||
<div style={{
|
|
||||||
width: '100%', maxWidth: '400px', backgroundColor: '#242424',
|
|
||||||
padding: '30px', borderTop: '4px solid #00E5FF', boxShadow: '0 10px 30px rgba(0,0,0,0.5)'
|
|
||||||
}}>
|
|
||||||
<h2 style={{ color: '#FFF', textTransform: 'uppercase', marginBottom: '20px', fontSize: '1.5rem' }}>
|
|
||||||
Authentification
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{/* Zone d'erreur qui s'affiche si le mot de passe est faux */}
|
|
||||||
{error && (
|
|
||||||
<div style={{ backgroundColor: 'rgba(255, 68, 68, 0.1)', color: '#FF4444', padding: '10px', marginBottom: '15px', border: '1px solid #FF4444', fontSize: '0.9rem' }}>
|
|
||||||
[ ERREUR ] : {error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleLogin}>
|
|
||||||
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>IDENTIFIANT (EMAIL)</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
style={inputStyle}
|
|
||||||
required
|
|
||||||
placeholder="admin@gise.be"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label style={{ display: 'block', color: '#888', marginBottom: '5px', fontSize: '0.8rem' }}>CLÉ D'ACCÈS (MOT DE PASSE)</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
style={inputStyle}
|
|
||||||
required
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button type="submit" style={buttonStyle} disabled={loading}>
|
|
||||||
{loading ? 'VÉRIFICATION...' : 'INITIALISER LA CONNEXION'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user