/* ============================================================ FLOWSHIELD — Login (autenticación real contra /api/auth/login) Login en dos pasos: contraseña -> (alta | verificación) TOTP. ============================================================ */ const { useState: useStateL, useEffect: useEffectL, useRef: useRefL } = React; function MatrixRain() { const ref = useRefL(null); useEffectL(() => { const cv = ref.current; if (!cv) return; const ctx = cv.getContext('2d'); let w, h, cols, drops; const chars = '01░▒▓<>{}[]/\\|=+*ABCDEF0123456789アカサタナ'; function resize(){ w = cv.width = cv.offsetWidth; h = cv.height = cv.offsetHeight; cols = Math.floor(w/16); drops = Array(cols).fill(0).map(()=>Math.random()*-50); } resize(); function draw(){ if (document.hidden) return; ctx.fillStyle = 'rgba(5,7,13,0.18)'; ctx.fillRect(0,0,w,h); ctx.font = '13px JetBrains Mono'; for (let i=0;i0.96 ? 'rgba(0,245,192,0.9)' : 'rgba(0,245,192,0.32)'; ctx.fillText(ch, x, y); if (y > h && Math.random()>0.975) drops[i]=0; drops[i]++; } } const timer = setInterval(draw, 70); window.addEventListener('resize', resize); return () => { clearInterval(timer); window.removeEventListener('resize', resize); }; }, []); return ; } function Login({ onAuth }) { const [u, setU] = useStateL(''); const [p, setP] = useStateL(''); const [show, setShow] = useStateL(false); const [err, setErr] = useStateL(''); const [busy, setBusy] = useStateL(false); const [boot, setBoot] = useStateL([]); // --- Segundo factor ------------------------------------------------------- const [fase, setFase] = useStateL('password'); // password | enroll | verify | recovery const [mfaToken, setMfaToken] = useStateL(''); const [qr, setQr] = useStateL(''); const [secreto, setSecreto] = useStateL(''); const [codigo, setCodigo] = useStateL(''); const [respaldo, setRespaldo] = useStateL(null); const [copiado, setCopiado] = useStateL(false); const cuentaRef = useRefL(null); // payload de cuenta pendiente de entregar // --- Turnstile ------------------------------------------------------------ const [sitekey, setSitekey] = useStateL(''); const turnstileRef = useRefL(null); const widgetId = useRefL(null); const BOOT = [ '> flowshield secure-shell v3.4.1', '> inicializando módulo criptográfico … OK', '> verificando integridad del kernel … OK', '> red team / blue team / normativa … cargados', '> esperando autenticación del operador_', ]; useEffectL(() => { let i = 0; const t = setInterval(() => { const cur = BOOT[i]; setBoot(b => [...b, cur]); i++; if (i >= BOOT.length) clearInterval(t); }, 380); return () => clearInterval(t); }, []); // La clave pública de Turnstile viaja en /api/health para poder rotarla sin // reconstruir el frontend. Si no hay clave, el backend tampoco exige captcha. useEffectL(() => { let alive = true; window.fsHealth().then(d => { if (alive && d && d.turnstile_sitekey) setSitekey(d.turnstile_sitekey); }).catch(() => {}); return () => { alive = false; }; }, []); // Monta el widget cuando estamos en la fase de contraseña y hay clave. El // script de Turnstile es `async`, asi que puede no estar listo cuando el // efecto corre (carrera con el fetch de la sitekey): se reintenta hasta que // `window.turnstile` exista, en vez de rendirse al primer intento. useEffectL(() => { if (fase !== 'password' || !sitekey) return; let cancel = false; const montar = () => { if (cancel || widgetId.current !== null) return; if (window.turnstile && turnstileRef.current) { try { widgetId.current = window.turnstile.render(turnstileRef.current, { sitekey }); } catch (e) { /* noop */ } } else { setTimeout(montar, 150); } }; montar(); return () => { cancel = true; }; }, [fase, sitekey]); const tokenTurnstile = () => { try { return (sitekey && window.turnstile && widgetId.current !== null) ? window.turnstile.getResponse(widgetId.current) : null; } catch (e) { return null; } }; const resetTurnstile = () => { try { if (window.turnstile && widgetId.current !== null) window.turnstile.reset(widgetId.current); } catch (e) { /* noop */ } }; const msgError = (ex, fallback) => (ex && ex.status === 401) ? fallback : (ex && ex.status === 403) ? 'Verificación anti-bot no superada · reintenta' : (ex && ex.status === 429) ? 'Demasiados intentos · espera unos minutos' : ('ERROR · ' + (ex && ex.message ? ex.message : 'no se pudo completar')); // Paso 1: contraseña. const submitPassword = async (e) => { e.preventDefault(); setErr(''); setBusy(true); try { const r = await window.fsAuth.login(u.trim(), p, tokenTurnstile()); setMfaToken(r.mfa_token); setCodigo(''); if (r.mfa === 'enroll') { const s = await window.fsAuth.mfaSetup(r.mfa_token); setQr(s.qr_svg); setSecreto(s.secret); setFase('enroll'); } else { setFase('verify'); } setBusy(false); } catch (ex) { setErr(msgError(ex, 'ACCESO DENEGADO · credenciales no válidas')); resetTurnstile(); setBusy(false); } }; // Paso 2a: confirmar el alta del segundo factor. const submitEnroll = async (e) => { e.preventDefault(); setErr(''); setBusy(true); try { const r = await window.fsAuth.mfaConfirm(mfaToken, codigo.trim()); cuentaRef.current = r; // ya lleva la sesión (cookie) puesta por el backend setRespaldo(r.recovery_codes || []); setFase('recovery'); setBusy(false); } catch (ex) { setErr(msgError(ex, 'Código no válido · revisa la app autenticadora')); setBusy(false); } }; // Paso 2b: verificar en logins posteriores (acepta código TOTP o de respaldo). const submitVerify = async (e) => { e.preventDefault(); setErr(''); setBusy(true); try { const r = await window.fsAuth.mfaVerify(mfaToken, codigo.trim()); setBoot(b => [...b, '> segundo factor verificado · acceso concedido ✓']); setTimeout(() => onAuth(r), 500); } catch (ex) { setErr(msgError(ex, 'Código no válido · vuelve a intentarlo')); setBusy(false); } }; const copiarRespaldo = () => { const txt = (respaldo || []).join('\n'); try { navigator.clipboard.writeText(txt).then(() => { setCopiado(true); setTimeout(() => setCopiado(false), 2000); }); } catch (e) { /* noop */ } }; const terminarAlta = () => { onAuth(cuentaRef.current || {}); }; // --- Cabeceras por fase --------------------------------------------------- const CAB = { password: { eyebrow:'// acceso restringido', titulo:'Autenticación', sub:'Sólo operadores autorizados. Modelo base.' }, enroll: { eyebrow:'// alta de segundo factor', titulo:'Configura tu 2FA', sub:'Escanea el QR con tu app autenticadora y escribe el código.' }, verify: { eyebrow:'// segundo factor', titulo:'Verificación en dos pasos', sub:'Introduce el código de tu app o uno de respaldo.' }, recovery: { eyebrow:'// códigos de respaldo', titulo:'Guarda estos códigos', sub:'Se muestran una sola vez. Guárdalos en tu gestor de contraseñas.' }, }[fase]; return (
{/* Left: brand + boot terminal */}
Plataforma unificada de operaciones de seguridad ofensiva, defensiva y cumplimiento normativo.
{boot.map((l, i) => (
{l}
))}
NODO SEGURO · ES-MAD-01
{/* Right: login form */}
{CAB.eyebrow}

{CAB.titulo}

{CAB.sub}

{fase === 'password' && (
setU(e.target.value)} autoFocus />
setP(e.target.value)} style={{ paddingRight:42 }} />
{err && (
{err}
)} {busy ? 'AUTENTICANDO…' : 'INICIAR SESIÓN'} )} {(fase === 'enroll' || fase === 'verify') && (
{fase === 'enroll' && (
{/* z-index por encima de .fx-scanline (9999): ese overlay multiply raya el QR y la camara no lo lee. */}
{secreto && (
o escribe la clave: {secreto}
)}
)}
setCodigo(e.target.value)} autoFocus />
{err && (
{err}
)} {busy ? 'VERIFICANDO…' : (fase === 'enroll' ? 'ACTIVAR 2FA' : 'VERIFICAR')} )} {fase === 'recovery' && (
{(respaldo || []).map((c, i) => (
{c}
))}
Cada código sirve una sola vez. Sin ellos, perder el móvil te deja fuera.
{copiado ? 'COPIADO ✓' : 'COPIAR CÓDIGOS'} YA LOS HE GUARDADO · ENTRAR
)}
2FA · cifrado AES-256 v3.4.1
FLOWSHIELD © 2026 · uso autorizado únicamente
); } Object.assign(window, { Login });