feat: Add 2FA frontend components (TwoFactorSetup, TwoFactorVerifyModal, TwoFactorSettings)

Created three complete 2FA components integrating with existing backend endpoints:

1. TwoFactorSetup.tsx (auth/components)
   - 3-step wizard for 2FA setup
   - QR code display with manual entry fallback
   - TOTP code verification
   - Backup codes display with copy functionality
   - Integrates with POST /auth/2fa/setup and /auth/2fa/enable

2. TwoFactorVerifyModal.tsx (auth/components)
   - Modal for 2FA verification during login
   - Support for both TOTP (6 digits) and backup codes (8 digits)
   - Auto-submit for TOTP codes
   - Switch between code types
   - Auto-focus input field

3. TwoFactorSettings.tsx (settings/components)
   - Management panel for 2FA in settings
   - Enable/disable 2FA with confirmation
   - Regenerate backup codes
   - Status display with activation date
   - Integrates with all 2FA endpoints

All components follow project patterns:
- Tailwind CSS styling matching existing components
- lucide-react icons
- Centralized apiClient with auto-refresh
- Complete TypeScript types
- No placeholders or TODOs

Related: GAP-P1-004 2FA Frontend Flow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-28 12:23:06 -06:00
parent 190decd498
commit 261dc4c71c
3 changed files with 1004 additions and 0 deletions

View File

@ -0,0 +1,364 @@
/**
* TwoFactorSetup Component
* Wizard for setting up 2FA with QR code and backup codes
*/
import React, { useState, useEffect } from 'react';
import { Shield, Smartphone, Copy, CheckCircle, AlertCircle, Loader2, ChevronRight, ChevronLeft } from 'lucide-react';
import { apiClient } from '../../../lib/apiClient';
interface TwoFactorSetupProps {
isOpen: boolean;
onClose: () => void;
onSuccess?: () => void;
}
interface SetupData {
secret: string;
qrCode: string;
backupCodes: string[];
}
type Step = 'qr' | 'verify' | 'backup';
export const TwoFactorSetup: React.FC<TwoFactorSetupProps> = ({
isOpen,
onClose,
onSuccess,
}) => {
const [step, setStep] = useState<Step>('qr');
const [setupData, setSetupData] = useState<SetupData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [totpCode, setTotpCode] = useState('');
const [verifying, setVerifying] = useState(false);
const [copiedCodes, setCopiedCodes] = useState(false);
// Fetch QR code when modal opens
useEffect(() => {
if (isOpen) {
fetchSetupData();
setStep('qr');
setTotpCode('');
setError(null);
setCopiedCodes(false);
}
}, [isOpen]);
const fetchSetupData = async () => {
setLoading(true);
setError(null);
try {
const response = await apiClient.post('/auth/2fa/setup');
if (response.data.success && response.data.data) {
setSetupData(response.data.data);
} else {
throw new Error('Invalid setup response');
}
} catch (err) {
console.error('Failed to fetch 2FA setup data:', err);
setError('Error al generar codigo QR. Intenta de nuevo.');
} finally {
setLoading(false);
}
};
const handleVerifyCode = async () => {
if (totpCode.length !== 6) {
setError('Ingresa un codigo de 6 digitos');
return;
}
setVerifying(true);
setError(null);
try {
const response = await apiClient.post('/auth/2fa/enable', {
token: totpCode,
});
if (response.data.success) {
setStep('backup');
} else {
throw new Error('Invalid verification response');
}
} catch (err: any) {
console.error('Failed to verify 2FA code:', err);
const errorMessage = err?.response?.data?.message || 'Codigo invalido. Verifica el codigo en tu app de autenticacion.';
setError(errorMessage);
} finally {
setVerifying(false);
}
};
const handleCopyBackupCodes = () => {
if (setupData?.backupCodes) {
const codesText = setupData.backupCodes.join('\n');
navigator.clipboard.writeText(codesText);
setCopiedCodes(true);
setTimeout(() => setCopiedCodes(false), 2000);
}
};
const handleFinish = () => {
onSuccess?.();
onClose();
};
const handleCodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 6);
setTotpCode(value);
if (error) setError(null);
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<div className="bg-slate-800 rounded-xl w-full max-w-lg overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-slate-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/20 rounded-lg">
<Shield className="w-5 h-5 text-blue-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Configurar Autenticacion 2FA</h2>
<p className="text-sm text-slate-400">
{step === 'qr' && 'Paso 1 de 3: Escanea el codigo QR'}
{step === 'verify' && 'Paso 2 de 3: Verifica el codigo'}
{step === 'backup' && 'Paso 3 de 3: Guarda tus codigos de respaldo'}
</p>
</div>
</div>
</div>
{/* Content */}
<div className="p-6">
{loading ? (
<div className="flex flex-col items-center justify-center py-12">
<Loader2 className="w-12 h-12 text-blue-500 animate-spin mb-4" />
<p className="text-slate-400">Generando codigo QR...</p>
</div>
) : (
<>
{/* Step 1: QR Code */}
{step === 'qr' && setupData && (
<div className="space-y-6">
<div className="bg-slate-900 rounded-lg p-6">
<div className="flex items-start gap-4">
<div className="p-2 bg-blue-500/20 rounded-lg flex-shrink-0">
<Smartphone className="w-5 h-5 text-blue-400" />
</div>
<div>
<h3 className="text-white font-medium mb-2">Configura tu app de autenticacion</h3>
<ol className="text-sm text-slate-400 space-y-2 list-decimal list-inside">
<li>Descarga una app de autenticacion (Google Authenticator, Authy, etc.)</li>
<li>Abre la app y selecciona "Agregar cuenta"</li>
<li>Escanea el codigo QR a continuacion</li>
</ol>
</div>
</div>
</div>
{/* QR Code */}
<div className="flex flex-col items-center justify-center py-6 bg-white rounded-lg">
<img
src={setupData.qrCode}
alt="QR Code para 2FA"
className="w-64 h-64"
/>
</div>
{/* Manual Entry */}
<div className="bg-slate-900 rounded-lg p-4">
<p className="text-xs text-slate-400 mb-2">
O ingresa manualmente este codigo:
</p>
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-slate-800 border border-slate-700 rounded text-sm text-white font-mono break-all">
{setupData.secret}
</code>
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(setupData.secret);
}}
className="p-2 text-slate-400 hover:text-white hover:bg-slate-700 rounded transition-colors"
title="Copiar codigo"
>
<Copy className="w-4 h-4" />
</button>
</div>
</div>
{error && (
<div className="flex items-center gap-2 p-3 bg-red-900/20 border border-red-800 rounded-lg">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
<p className="text-sm text-red-400">{error}</p>
</div>
)}
<button
type="button"
onClick={() => setStep('verify')}
className="w-full py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
>
Siguiente
<ChevronRight className="w-5 h-5" />
</button>
</div>
)}
{/* Step 2: Verify Code */}
{step === 'verify' && (
<div className="space-y-6">
<div className="bg-slate-900 rounded-lg p-6 text-center">
<div className="w-16 h-16 bg-blue-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<Shield className="w-8 h-8 text-blue-400" />
</div>
<h3 className="text-white font-medium mb-2">Verifica tu configuracion</h3>
<p className="text-sm text-slate-400">
Ingresa el codigo de 6 digitos que aparece en tu app de autenticacion
</p>
</div>
<div>
<label className="block text-sm text-slate-400 mb-2">Codigo TOTP</label>
<input
type="text"
inputMode="numeric"
pattern="[0-9]*"
maxLength={6}
value={totpCode}
onChange={handleCodeChange}
placeholder="000000"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-lg text-white text-center text-2xl font-mono tracking-widest focus:outline-none focus:border-blue-500"
autoFocus
/>
</div>
{error && (
<div className="flex items-center gap-2 p-3 bg-red-900/20 border border-red-800 rounded-lg">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
<p className="text-sm text-red-400">{error}</p>
</div>
)}
<div className="flex gap-3">
<button
type="button"
onClick={() => setStep('qr')}
className="flex-1 py-3 bg-slate-700 hover:bg-slate-600 text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
>
<ChevronLeft className="w-5 h-5" />
Atras
</button>
<button
type="button"
onClick={handleVerifyCode}
disabled={verifying || totpCode.length !== 6}
className="flex-1 py-3 bg-blue-600 hover:bg-blue-500 disabled:bg-slate-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
>
{verifying ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Verificando...
</>
) : (
<>
Verificar
<ChevronRight className="w-5 h-5" />
</>
)}
</button>
</div>
</div>
)}
{/* Step 3: Backup Codes */}
{step === 'backup' && setupData && (
<div className="space-y-6">
<div className="bg-emerald-900/20 border border-emerald-800 rounded-lg p-6">
<div className="flex items-start gap-3">
<CheckCircle className="w-6 h-6 text-emerald-400 flex-shrink-0 mt-0.5" />
<div>
<h3 className="text-white font-medium mb-1">2FA activado exitosamente</h3>
<p className="text-sm text-slate-400">
Guarda estos codigos de respaldo en un lugar seguro. Los necesitaras si pierdes acceso a tu app de autenticacion.
</p>
</div>
</div>
</div>
<div className="bg-slate-900 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-white font-medium">Codigos de Respaldo</h4>
<button
type="button"
onClick={handleCopyBackupCodes}
className="flex items-center gap-2 px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-slate-300 rounded text-sm transition-colors"
>
{copiedCodes ? (
<>
<CheckCircle className="w-4 h-4 text-emerald-400" />
Copiado
</>
) : (
<>
<Copy className="w-4 h-4" />
Copiar todos
</>
)}
</button>
</div>
<div className="grid grid-cols-2 gap-3">
{setupData.backupCodes.map((code, index) => (
<div
key={index}
className="px-4 py-3 bg-slate-800 border border-slate-700 rounded-lg"
>
<code className="text-sm text-white font-mono">{code}</code>
</div>
))}
</div>
<div className="mt-4 p-3 bg-amber-900/20 border border-amber-800 rounded-lg">
<p className="text-xs text-amber-400">
<strong>Importante:</strong> Cada codigo solo puede usarse una vez. Guardalo en un lugar seguro y accesible.
</p>
</div>
</div>
<button
type="button"
onClick={handleFinish}
className="w-full py-3 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
>
Finalizar
</button>
</div>
)}
</>
)}
</div>
{/* Footer */}
{!loading && (
<div className="px-6 pb-6">
<button
type="button"
onClick={onClose}
className="w-full py-2 text-slate-400 hover:text-white text-sm transition-colors"
>
Cancelar
</button>
</div>
)}
</div>
</div>
);
};
export default TwoFactorSetup;

View File

@ -0,0 +1,234 @@
/**
* TwoFactorVerifyModal Component
* Modal for verifying 2FA code during login
*/
import React, { useState, useEffect, useRef } from 'react';
import { Shield, AlertCircle, Loader2, Key } from 'lucide-react';
import { apiClient } from '../../../lib/apiClient';
interface TwoFactorVerifyModalProps {
isOpen: boolean;
onClose: () => void;
onVerify: (token: string) => Promise<void>;
sessionToken: string;
}
type InputMode = 'totp' | 'backup';
export const TwoFactorVerifyModal: React.FC<TwoFactorVerifyModalProps> = ({
isOpen,
onClose,
onVerify,
sessionToken,
}) => {
const [mode, setMode] = useState<InputMode>('totp');
const [code, setCode] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
// Reset state when modal opens
useEffect(() => {
if (isOpen) {
setMode('totp');
setCode('');
setError(null);
setLoading(false);
setTimeout(() => inputRef.current?.focus(), 100);
}
}, [isOpen]);
// Auto-submit when TOTP code is complete
useEffect(() => {
if (mode === 'totp' && code.length === 6 && !loading) {
handleSubmit();
}
}, [code, mode]);
const handleSubmit = async () => {
const expectedLength = mode === 'totp' ? 6 : 8;
if (code.length !== expectedLength) {
setError(`Ingresa un codigo de ${expectedLength} digitos`);
return;
}
setLoading(true);
setError(null);
try {
await onVerify(code);
} catch (err: any) {
console.error('Failed to verify 2FA:', err);
const errorMessage = err?.response?.data?.message ||
mode === 'totp'
? 'Codigo TOTP invalido. Intenta de nuevo.'
: 'Codigo de respaldo invalido o ya usado.';
setError(errorMessage);
setCode('');
setTimeout(() => inputRef.current?.focus(), 100);
} finally {
setLoading(false);
}
};
const handleCodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const maxLength = mode === 'totp' ? 6 : 8;
const value = e.target.value.replace(/\D/g, '').slice(0, maxLength);
setCode(value);
if (error) setError(null);
};
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && !loading) {
e.preventDefault();
handleSubmit();
}
};
const switchMode = () => {
setMode(mode === 'totp' ? 'backup' : 'totp');
setCode('');
setError(null);
setTimeout(() => inputRef.current?.focus(), 100);
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<div className="bg-slate-800 rounded-xl w-full max-w-md overflow-hidden">
{/* Header */}
<div className="flex items-center gap-3 p-6 border-b border-slate-700">
<div className="p-2 bg-blue-500/20 rounded-lg">
<Shield className="w-5 h-5 text-blue-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Verificacion de Dos Factores</h2>
<p className="text-sm text-slate-400">
{mode === 'totp'
? 'Ingresa el codigo de tu app de autenticacion'
: 'Ingresa uno de tus codigos de respaldo'
}
</p>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{/* Icon */}
<div className="flex justify-center">
<div className="w-16 h-16 bg-blue-500/20 rounded-full flex items-center justify-center">
{mode === 'totp' ? (
<Shield className="w-8 h-8 text-blue-400" />
) : (
<Key className="w-8 h-8 text-blue-400" />
)}
</div>
</div>
{/* Code Input */}
<div>
<label className="block text-sm text-slate-400 mb-2 text-center">
{mode === 'totp' ? 'Codigo TOTP (6 digitos)' : 'Codigo de Respaldo (8 digitos)'}
</label>
<input
ref={inputRef}
type="text"
inputMode="numeric"
pattern="[0-9]*"
maxLength={mode === 'totp' ? 6 : 8}
value={code}
onChange={handleCodeChange}
onKeyPress={handleKeyPress}
placeholder={mode === 'totp' ? '000000' : '00000000'}
disabled={loading}
className="w-full px-4 py-4 bg-slate-900 border border-slate-700 rounded-lg text-white text-center text-2xl font-mono tracking-widest focus:outline-none focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
autoFocus
/>
</div>
{/* Error Message */}
{error && (
<div className="flex items-center gap-2 p-3 bg-red-900/20 border border-red-800 rounded-lg">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
<p className="text-sm text-red-400">{error}</p>
</div>
)}
{/* Loading State */}
{loading && (
<div className="flex items-center justify-center gap-2 text-blue-400">
<Loader2 className="w-5 h-5 animate-spin" />
<span className="text-sm">Verificando...</span>
</div>
)}
{/* Submit Button (for backup code mode) */}
{mode === 'backup' && (
<button
type="button"
onClick={handleSubmit}
disabled={loading || code.length !== 8}
className="w-full py-3 bg-blue-600 hover:bg-blue-500 disabled:bg-slate-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
>
{loading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Verificando...
</>
) : (
'Verificar'
)}
</button>
)}
{/* Switch Mode Link */}
<div className="text-center">
<button
type="button"
onClick={switchMode}
disabled={loading}
className="text-sm text-blue-400 hover:text-blue-300 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{mode === 'totp'
? 'Usar codigo de respaldo'
: 'Usar codigo de autenticacion'
}
</button>
</div>
{/* Info */}
<div className="bg-slate-900 rounded-lg p-4">
<p className="text-xs text-slate-400 text-center">
{mode === 'totp' ? (
<>
El codigo cambia cada 30 segundos en tu app de autenticacion (Google Authenticator, Authy, etc.)
</>
) : (
<>
Los codigos de respaldo se generaron cuando activaste 2FA. Cada codigo solo puede usarse una vez.
</>
)}
</p>
</div>
</div>
{/* Footer */}
<div className="px-6 pb-6">
<button
type="button"
onClick={onClose}
disabled={loading}
className="w-full py-2 text-slate-400 hover:text-white text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancelar
</button>
</div>
</div>
</div>
);
};
export default TwoFactorVerifyModal;

View File

@ -0,0 +1,406 @@
/**
* TwoFactorSettings Component
* Panel for managing 2FA settings in user settings page
*/
import React, { useState, useEffect } from 'react';
import { Shield, AlertCircle, CheckCircle, Loader2, RefreshCw, ShieldAlert, ShieldCheck } from 'lucide-react';
import { apiClient } from '../../../lib/apiClient';
import TwoFactorSetup from '../../auth/components/TwoFactorSetup';
interface TwoFactorStatus {
enabled: boolean;
enabledAt?: string;
}
export const TwoFactorSettings: React.FC = () => {
const [status, setStatus] = useState<TwoFactorStatus | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showSetup, setShowSetup] = useState(false);
const [showDisableConfirm, setShowDisableConfirm] = useState(false);
const [disableCode, setDisableCode] = useState('');
const [disabling, setDisabling] = useState(false);
const [showRegenerateConfirm, setShowRegenerateConfirm] = useState(false);
const [regenerating, setRegenerating] = useState(false);
const [regeneratedCodes, setRegeneratedCodes] = useState<string[] | null>(null);
useEffect(() => {
fetchStatus();
}, []);
const fetchStatus = async () => {
setLoading(true);
setError(null);
try {
const response = await apiClient.get('/auth/2fa/status');
if (response.data.success && response.data.data) {
setStatus(response.data.data);
}
} catch (err) {
console.error('Failed to fetch 2FA status:', err);
setError('Error al cargar el estado de 2FA');
} finally {
setLoading(false);
}
};
const handleEnableSuccess = () => {
setShowSetup(false);
fetchStatus();
};
const handleDisable = async () => {
if (disableCode.length !== 6) {
setError('Ingresa un codigo de 6 digitos');
return;
}
setDisabling(true);
setError(null);
try {
const response = await apiClient.post('/auth/2fa/disable', {
token: disableCode,
});
if (response.data.success) {
setShowDisableConfirm(false);
setDisableCode('');
await fetchStatus();
}
} catch (err: any) {
console.error('Failed to disable 2FA:', err);
const errorMessage = err?.response?.data?.message || 'Error al desactivar 2FA. Verifica el codigo.';
setError(errorMessage);
} finally {
setDisabling(false);
}
};
const handleRegenerateBackupCodes = async () => {
setRegenerating(true);
setError(null);
try {
const response = await apiClient.post('/auth/2fa/backup-codes');
if (response.data.success && response.data.data?.backupCodes) {
setRegeneratedCodes(response.data.data.backupCodes);
setShowRegenerateConfirm(false);
}
} catch (err: any) {
console.error('Failed to regenerate backup codes:', err);
const errorMessage = err?.response?.data?.message || 'Error al regenerar codigos de respaldo';
setError(errorMessage);
} finally {
setRegenerating(false);
}
};
const handleCopyRegeneratedCodes = () => {
if (regeneratedCodes) {
const codesText = regeneratedCodes.join('\n');
navigator.clipboard.writeText(codesText);
}
};
const handleCloseRegeneratedCodes = () => {
setRegeneratedCodes(null);
};
if (loading) {
return (
<div className="card">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-blue-500/20 rounded-lg">
<Shield className="w-5 h-5 text-blue-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Autenticacion de Dos Factores</h2>
<p className="text-sm text-slate-400">Cargando configuracion...</p>
</div>
</div>
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 text-blue-500 animate-spin" />
</div>
</div>
);
}
return (
<>
<div className="card">
<div className="flex items-start justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/20 rounded-lg">
<Shield className="w-5 h-5 text-blue-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Autenticacion de Dos Factores</h2>
<p className="text-sm text-slate-400">
Agrega una capa extra de seguridad a tu cuenta
</p>
</div>
</div>
{status?.enabled && (
<div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-500/20 rounded-full">
<ShieldCheck className="w-4 h-4 text-emerald-400" />
<span className="text-sm font-medium text-emerald-400">Activo</span>
</div>
)}
</div>
{error && (
<div className="flex items-center gap-2 p-3 bg-red-900/20 border border-red-800 rounded-lg mb-4">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
<p className="text-sm text-red-400">{error}</p>
</div>
)}
{status?.enabled ? (
<div className="space-y-4">
{/* Enabled Status */}
<div className="bg-emerald-900/20 border border-emerald-800 rounded-lg p-4">
<div className="flex items-start gap-3">
<CheckCircle className="w-5 h-5 text-emerald-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-white font-medium mb-1">2FA esta activo</p>
<p className="text-sm text-slate-400">
Tu cuenta esta protegida con autenticacion de dos factores.
{status.enabledAt && (
<> Activado el {new Date(status.enabledAt).toLocaleDateString()}.</>
)}
</p>
</div>
</div>
</div>
{/* Actions */}
<div className="grid gap-3">
{/* Regenerate Backup Codes */}
{!showRegenerateConfirm ? (
<button
type="button"
onClick={() => setShowRegenerateConfirm(true)}
className="flex items-center justify-between p-4 bg-slate-700 hover:bg-slate-600 rounded-lg transition-colors"
>
<div className="flex items-center gap-3">
<RefreshCw className="w-5 h-5 text-blue-400" />
<div className="text-left">
<p className="text-white font-medium">Regenerar Codigos de Respaldo</p>
<p className="text-sm text-slate-400">
Genera nuevos codigos si perdiste los anteriores
</p>
</div>
</div>
</button>
) : (
<div className="p-4 bg-amber-900/20 border border-amber-800 rounded-lg space-y-3">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-white font-medium mb-1">Confirmar regeneracion</p>
<p className="text-sm text-amber-400">
Esto invalidara todos tus codigos de respaldo anteriores.
</p>
</div>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={handleRegenerateBackupCodes}
disabled={regenerating}
className="flex-1 py-2 bg-amber-600 hover:bg-amber-500 disabled:bg-slate-600 disabled:cursor-not-allowed text-white rounded-lg text-sm font-medium transition-colors"
>
{regenerating ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
Regenerando...
</span>
) : (
'Confirmar'
)}
</button>
<button
type="button"
onClick={() => setShowRegenerateConfirm(false)}
disabled={regenerating}
className="flex-1 py-2 bg-slate-700 hover:bg-slate-600 disabled:cursor-not-allowed text-slate-300 rounded-lg text-sm font-medium transition-colors"
>
Cancelar
</button>
</div>
</div>
)}
{/* Disable 2FA */}
{!showDisableConfirm ? (
<button
type="button"
onClick={() => setShowDisableConfirm(true)}
className="flex items-center justify-between p-4 bg-red-900/20 border border-red-800 hover:bg-red-900/30 rounded-lg transition-colors"
>
<div className="flex items-center gap-3">
<ShieldAlert className="w-5 h-5 text-red-400" />
<div className="text-left">
<p className="text-white font-medium">Desactivar 2FA</p>
<p className="text-sm text-red-400">
Reduce la seguridad de tu cuenta
</p>
</div>
</div>
</button>
) : (
<div className="p-4 bg-red-900/20 border border-red-800 rounded-lg space-y-3">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-white font-medium mb-1">Confirmar desactivacion</p>
<p className="text-sm text-red-400">
Ingresa tu codigo TOTP actual para desactivar 2FA
</p>
</div>
</div>
<input
type="text"
inputMode="numeric"
pattern="[0-9]*"
maxLength={6}
value={disableCode}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 6);
setDisableCode(value);
if (error) setError(null);
}}
placeholder="000000"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-lg text-white text-center text-lg font-mono tracking-widest focus:outline-none focus:border-red-500"
autoFocus
/>
<div className="flex gap-2">
<button
type="button"
onClick={handleDisable}
disabled={disabling || disableCode.length !== 6}
className="flex-1 py-2 bg-red-600 hover:bg-red-500 disabled:bg-slate-600 disabled:cursor-not-allowed text-white rounded-lg text-sm font-medium transition-colors"
>
{disabling ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
Desactivando...
</span>
) : (
'Desactivar'
)}
</button>
<button
type="button"
onClick={() => {
setShowDisableConfirm(false);
setDisableCode('');
setError(null);
}}
disabled={disabling}
className="flex-1 py-2 bg-slate-700 hover:bg-slate-600 disabled:cursor-not-allowed text-slate-300 rounded-lg text-sm font-medium transition-colors"
>
Cancelar
</button>
</div>
</div>
)}
</div>
</div>
) : (
<div className="space-y-4">
{/* Disabled Status */}
<div className="bg-slate-900 rounded-lg p-4">
<div className="flex items-start gap-3">
<ShieldAlert className="w-5 h-5 text-slate-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-white font-medium mb-1">2FA no esta activo</p>
<p className="text-sm text-slate-400">
Protege tu cuenta con autenticacion de dos factores. Necesitaras una app de autenticacion como Google Authenticator o Authy.
</p>
</div>
</div>
</div>
{/* Enable Button */}
<button
type="button"
onClick={() => setShowSetup(true)}
className="w-full py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
>
<Shield className="w-5 h-5" />
Activar 2FA
</button>
{/* Info */}
<div className="bg-blue-900/20 border border-blue-800 rounded-lg p-4">
<p className="text-sm text-blue-400">
<strong>Recomendado:</strong> La autenticacion de dos factores agrega una capa adicional de seguridad requiriendo un codigo temporal ademas de tu contrasena.
</p>
</div>
</div>
)}
</div>
{/* Setup Modal */}
<TwoFactorSetup
isOpen={showSetup}
onClose={() => setShowSetup(false)}
onSuccess={handleEnableSuccess}
/>
{/* Regenerated Codes Modal */}
{regeneratedCodes && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<div className="bg-slate-800 rounded-xl w-full max-w-md overflow-hidden">
<div className="p-6 space-y-4">
<div className="flex items-start gap-3">
<CheckCircle className="w-6 h-6 text-emerald-400 flex-shrink-0 mt-0.5" />
<div>
<h3 className="text-lg font-semibold text-white mb-1">Nuevos Codigos Generados</h3>
<p className="text-sm text-slate-400">
Guarda estos codigos en un lugar seguro. Los anteriores ya no funcionan.
</p>
</div>
</div>
<div className="bg-slate-900 rounded-lg p-4">
<div className="grid grid-cols-2 gap-3">
{regeneratedCodes.map((code, index) => (
<div
key={index}
className="px-4 py-3 bg-slate-800 border border-slate-700 rounded-lg"
>
<code className="text-sm text-white font-mono">{code}</code>
</div>
))}
</div>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={handleCopyRegeneratedCodes}
className="flex-1 py-2 bg-slate-700 hover:bg-slate-600 text-slate-300 rounded-lg text-sm font-medium transition-colors"
>
Copiar Todos
</button>
<button
type="button"
onClick={handleCloseRegeneratedCodes}
className="flex-1 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm font-medium transition-colors"
>
Cerrar
</button>
</div>
</div>
</div>
</div>
)}
</>
);
};
export default TwoFactorSettings;