// AGENTFORGE-IA — Modal de autenticación (login / registro) const AuthModal = ({ isOpen, onClose, onSuccess }) => { const [tab, setTab] = React.useState('login'); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(''); // MFA second step const [mfaStep, setMfaStep] = React.useState(false); const [mfaToken, setMfaToken] = React.useState(''); const [mfaCode, setMfaCode] = React.useState(''); const [mfaSetupStep, setMfaSetupStep] = React.useState(false); const [mfaSetupCode, setMfaSetupCode] = React.useState(''); const [mfaSetup, setMfaSetup] = React.useState({ qr_svg: '', secret: '', totp_uri: '' }); // Login state const [loginEmail, setLoginEmail] = React.useState(''); const [loginPass, setLoginPass] = React.useState(''); const [showLoginPass, setShowLoginPass] = React.useState(false); // Register state const [regName, setRegName] = React.useState(''); const [regEmail, setRegEmail] = React.useState(''); const [regCompany, setRegCompany] = React.useState(''); const [regPass, setRegPass] = React.useState(''); const [regPassConfirm, setRegPassConfirm] = React.useState(''); const [showRegPass, setShowRegPass] = React.useState(false); const [showRegPassConfirm, setShowRegPassConfirm] = React.useState(false); const resetError = () => setError(''); /** * Persiste el usuario autenticado en sessionStorage. * @param {Record} user Perfil del usuario autenticado. * @returns {void} */ const persistSessionUser = (user) => { sessionStorage.setItem('af_user', JSON.stringify(user)); if (user && user.id) { sessionStorage.setItem('af_customer_id', String(user.id)); } }; const handleTabChange = (t) => { setTab(t); setError(''); setMfaStep(false); setMfaToken(''); setMfaCode(''); setMfaSetupStep(false); setMfaSetupCode(''); setMfaSetup({ qr_svg: '', secret: '', totp_uri: '' }); }; const passMatch = regPass.length > 0 && regPassConfirm.length > 0 && regPass === regPassConfirm; const passMismatch = regPass.length > 0 && regPassConfirm.length > 0 && regPass !== regPassConfirm; const handleLogin = async (e) => { e.preventDefault(); setError(''); if (!loginEmail.trim() || !loginPass) { setError('Email y contraseña son obligatorios.'); return; } setLoading(true); try { const res = await fetch('/api/v1/customers/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: loginEmail.trim(), password: loginPass }), }); const data = await res.json(); if (!res.ok) { setError(data.detail || 'Credenciales incorrectas.'); return; } if (data.mfa_required) { setMfaToken(data.mfa_token); setMfaStep(true); return; } const token = data.access_token; sessionStorage.setItem('af_token', token); if (data.mfa_setup_required) { const setupRes = await fetch('/api/v1/customers/mfa/setup', { method: 'POST', headers: { Authorization: 'Bearer ' + token }, }); const setupData = await setupRes.json(); if (!setupRes.ok) { setError(setupData.detail || 'No se pudo iniciar MFA.'); return; } setMfaSetup({ qr_svg: setupData.qr_svg || '', secret: setupData.secret || '', totp_uri: setupData.totp_uri || '' }); setMfaSetupStep(true); return; } const meRes = await fetch('/api/v1/customers/me', { headers: { Authorization: 'Bearer ' + token }, }); if (!meRes.ok) { setError('No se pudo obtener el perfil de usuario.'); return; } const user = await meRes.json(); persistSessionUser(user); onSuccess(user); } catch (_) { setError('Error de red. Comprueba tu conexión.'); } finally { setLoading(false); } }; const handleMfaVerify = async (e) => { e.preventDefault(); setError(''); if (mfaCode.length !== 6) { setError('Introduce el código de 6 dígitos de tu app.'); return; } setLoading(true); try { const res = await fetch('/api/v1/customers/mfa/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mfa_token: mfaToken, code: mfaCode }), }); const data = await res.json(); if (!res.ok) { setError(data.detail || 'Código incorrecto.'); return; } const token = data.access_token; sessionStorage.setItem('af_token', token); const meRes = await fetch('/api/v1/customers/me', { headers: { Authorization: 'Bearer ' + token }, }); if (!meRes.ok) { setError('No se pudo obtener el perfil.'); return; } const user = await meRes.json(); persistSessionUser(user); onSuccess(user); } catch (_) { setError('Error de red. Comprueba tu conexión.'); } finally { setLoading(false); } }; const handleMfaSetupActivate = async (e) => { e.preventDefault(); setError(''); if (mfaSetupCode.length !== 6) { setError('Introduce el código de 6 dígitos de tu app.'); return; } const token = sessionStorage.getItem('af_token') || ''; if (!token) { setError('Sesión inválida. Vuelve a iniciar sesión.'); return; } setLoading(true); try { const res = await fetch('/api/v1/customers/mfa/activate', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify({ code: mfaSetupCode }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { setError(data.detail || 'No se pudo activar MFA.'); return; } const meRes = await fetch('/api/v1/customers/me', { headers: { Authorization: 'Bearer ' + token }, }); if (!meRes.ok) { setError('MFA activado, pero no se pudo cargar perfil.'); return; } const user = await meRes.json(); persistSessionUser(user); onSuccess(user); } catch (_) { setError('Error de red. Comprueba tu conexión.'); } finally { setLoading(false); } }; const handleRegister = async (e) => { e.preventDefault(); setError(''); if (!regName.trim() || !regEmail.trim() || !regPass) { setError('Nombre, email y contraseña son obligatorios.'); return; } if (!regPassConfirm) { setError('Confirma tu contraseña.'); return; } if (regPass !== regPassConfirm) { setError('Las contraseñas no coinciden. Compruébalas e inténtalo de nuevo.'); return; } setLoading(true); try { const body = { full_name: regName.trim(), email: regEmail.trim(), password: regPass }; if (regCompany.trim()) body.company = regCompany.trim(); const res = await fetch('/api/v1/customers/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok) { setError(data.detail || 'No se pudo crear la cuenta.'); return; } const loginRes = await fetch('/api/v1/customers/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: regEmail.trim(), password: regPass }), }); const loginData = await loginRes.json(); if (!loginRes.ok) { setError('Cuenta creada. Inicia sesión manualmente.'); setTab('login'); return; } const token = loginData.access_token; sessionStorage.setItem('af_token', token); if (loginData.mfa_setup_required) { const setupRes = await fetch('/api/v1/customers/mfa/setup', { method: 'POST', headers: { Authorization: 'Bearer ' + token }, }); const setupData = await setupRes.json(); if (!setupRes.ok) { setError(setupData.detail || 'No se pudo iniciar MFA.'); return; } setMfaSetup({ qr_svg: setupData.qr_svg || '', secret: setupData.secret || '', totp_uri: setupData.totp_uri || '' }); setMfaSetupStep(true); return; } const meRes = await fetch('/api/v1/customers/me', { headers: { Authorization: 'Bearer ' + token }, }); const user = await meRes.json(); persistSessionUser(user); onSuccess(user); } catch (_) { setError('Error de red. Comprueba tu conexión.'); } finally { setLoading(false); } }; if (!isOpen) return null; const overlayStyle = { position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.85)', backdropFilter: 'blur(6px)', display: 'flex', alignItems: 'center', justifyContent: 'center', }; const cardStyle = { background: '#1a1a1a', border: '1px solid #2a2a2a', borderRadius: 12, padding: '32px 28px', width: '100%', maxWidth: 420, boxShadow: '0 24px 64px rgba(0,0,0,0.6)', fontFamily: '"Inter Tight", "Inter", sans-serif', color: '#e5e5e5', }; const tabsStyle = { display: 'flex', gap: 0, marginBottom: 24, background: '#111', borderRadius: 8, padding: 3, border: '1px solid #2a2a2a', }; const tabBtnStyle = (active) => ({ flex: 1, padding: '7px 0', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: 13, fontWeight: 600, fontFamily: 'inherit', transition: 'background 0.15s, color 0.15s', background: active ? '#ff5b1f' : 'transparent', color: active ? '#fff' : '#888', }); const labelStyle = { display: 'block', fontSize: 11.5, fontWeight: 600, color: '#888', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 5, }; const inputStyle = { width: '100%', boxSizing: 'border-box', background: '#111', border: '1px solid #2a2a2a', borderRadius: 7, padding: '9px 12px', fontSize: 13.5, color: '#e5e5e5', fontFamily: 'inherit', outline: 'none', transition: 'border-color 0.15s', }; const passInputStyle = { ...inputStyle, paddingRight: 40 }; const passWrapStyle = { position: 'relative' }; const eyeBtnStyle = { position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', color: '#555', padding: 2, display: 'flex', alignItems: 'center', lineHeight: 1, }; const fieldStyle = { marginBottom: 14 }; const submitBtnStyle = { width: '100%', padding: '10px 0', background: '#ff5b1f', color: '#fff', border: 'none', borderRadius: 7, fontSize: 14, fontWeight: 700, fontFamily: 'inherit', cursor: loading ? 'not-allowed' : 'pointer', opacity: loading ? 0.7 : 1, marginTop: 4, transition: 'opacity 0.15s', }; const errorStyle = { marginTop: 12, padding: '9px 12px', background: 'rgba(255,59,59,0.1)', border: '1px solid rgba(255,59,59,0.3)', borderRadius: 6, fontSize: 12.5, color: '#ff6b6b', lineHeight: 1.5, }; const closeBtnStyle = { position: 'absolute', top: 14, right: 14, background: 'none', border: 'none', color: '#888', cursor: 'pointer', padding: 4, borderRadius: 4, fontSize: 18, lineHeight: 1, }; const headingStyle = { margin: '0 0 20px', fontSize: 18, fontWeight: 700, letterSpacing: '-0.02em', color: '#e5e5e5', }; const optionalStyle = { color: '#555', fontWeight: 400, fontSize: 10, textTransform: 'lowercase', letterSpacing: 0, }; const passFeedbackStyle = (ok) => ({ marginTop: 5, fontSize: 11.5, color: ok ? '#34d399' : '#ff6b6b', display: 'flex', alignItems: 'center', gap: 4, }); return (
{ if (e.target === e.currentTarget) onClose(); }}>
{mfaSetupStep && (

Activa MFA (recomendado)

Escanea el QR con Microsoft Authenticator o Google Authenticator, y escribe el código de 6 dígitos.

{mfaSetup.qr_svg ? (QR MFA) : (QR no disponible)}
Clave manual: {mfaSetup.secret}
{ setMfaSetupCode(e.target.value.replace(/\D/g, '')); resetError(); }} autoFocus autoComplete="one-time-code" />
{error &&
{error}
}
)} {mfaStep && (

Verificación en dos pasos

Abre tu app de autenticación e introduce
el código de 6 dígitos.

{ setMfaCode(e.target.value.replace(/\D/g, '')); resetError(); }} autoFocus autoComplete="one-time-code" />
{error &&
{error}
}
)} {!mfaStep && !mfaSetupStep && tab === 'login' && (

Bienvenido de nuevo

{ setLoginEmail(e.target.value); resetError(); }} autoComplete="email" autoFocus />
{ setLoginPass(e.target.value); resetError(); }} autoComplete="current-password" />
{error &&
{error}
}
)} {!mfaStep && !mfaSetupStep && tab === 'register' && (

Crea tu cuenta

{ setRegName(e.target.value); resetError(); }} autoComplete="name" autoFocus />
{ setRegEmail(e.target.value); resetError(); }} autoComplete="email" />
{ setRegCompany(e.target.value); resetError(); }} autoComplete="organization" />
{ setRegPass(e.target.value); resetError(); }} autoComplete="new-password" />
{ setRegPassConfirm(e.target.value); resetError(); }} autoComplete="new-password" />
{passMatch && (
Las contraseñas coinciden
)} {passMismatch && (
Las contraseñas no coinciden
)}
{error &&
{error}
}
)}
); }; window.AuthModal = AuthModal;