diff --git a/src/modules/auth/components/TwoFactorSetup.tsx b/src/modules/auth/components/TwoFactorSetup.tsx new file mode 100644 index 0000000..ab21fc8 --- /dev/null +++ b/src/modules/auth/components/TwoFactorSetup.tsx @@ -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 = ({ + isOpen, + onClose, + onSuccess, +}) => { + const [step, setStep] = useState('qr'); + const [setupData, setSetupData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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) => { + const value = e.target.value.replace(/\D/g, '').slice(0, 6); + setTotpCode(value); + if (error) setError(null); + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+

Configurar Autenticacion 2FA

+

+ {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'} +

+
+
+
+ + {/* Content */} +
+ {loading ? ( +
+ +

Generando codigo QR...

+
+ ) : ( + <> + {/* Step 1: QR Code */} + {step === 'qr' && setupData && ( +
+
+
+
+ +
+
+

Configura tu app de autenticacion

+
    +
  1. Descarga una app de autenticacion (Google Authenticator, Authy, etc.)
  2. +
  3. Abre la app y selecciona "Agregar cuenta"
  4. +
  5. Escanea el codigo QR a continuacion
  6. +
+
+
+
+ + {/* QR Code */} +
+ QR Code para 2FA +
+ + {/* Manual Entry */} +
+

+ O ingresa manualmente este codigo: +

+
+ + {setupData.secret} + + +
+
+ + {error && ( +
+ +

{error}

+
+ )} + + +
+ )} + + {/* Step 2: Verify Code */} + {step === 'verify' && ( +
+
+
+ +
+

Verifica tu configuracion

+

+ Ingresa el codigo de 6 digitos que aparece en tu app de autenticacion +

+
+ +
+ + +
+ + {error && ( +
+ +

{error}

+
+ )} + +
+ + +
+
+ )} + + {/* Step 3: Backup Codes */} + {step === 'backup' && setupData && ( +
+
+
+ +
+

2FA activado exitosamente

+

+ Guarda estos codigos de respaldo en un lugar seguro. Los necesitaras si pierdes acceso a tu app de autenticacion. +

+
+
+
+ +
+
+

Codigos de Respaldo

+ +
+ +
+ {setupData.backupCodes.map((code, index) => ( +
+ {code} +
+ ))} +
+ +
+

+ Importante: Cada codigo solo puede usarse una vez. Guardalo en un lugar seguro y accesible. +

+
+
+ + +
+ )} + + )} +
+ + {/* Footer */} + {!loading && ( +
+ +
+ )} +
+
+ ); +}; + +export default TwoFactorSetup; diff --git a/src/modules/auth/components/TwoFactorVerifyModal.tsx b/src/modules/auth/components/TwoFactorVerifyModal.tsx new file mode 100644 index 0000000..6eeade5 --- /dev/null +++ b/src/modules/auth/components/TwoFactorVerifyModal.tsx @@ -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; + sessionToken: string; +} + +type InputMode = 'totp' | 'backup'; + +export const TwoFactorVerifyModal: React.FC = ({ + isOpen, + onClose, + onVerify, + sessionToken, +}) => { + const [mode, setMode] = useState('totp'); + const [code, setCode] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const inputRef = useRef(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) => { + 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) => { + 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 ( +
+
+ {/* Header */} +
+
+ +
+
+

Verificacion de Dos Factores

+

+ {mode === 'totp' + ? 'Ingresa el codigo de tu app de autenticacion' + : 'Ingresa uno de tus codigos de respaldo' + } +

+
+
+ + {/* Content */} +
+ {/* Icon */} +
+
+ {mode === 'totp' ? ( + + ) : ( + + )} +
+
+ + {/* Code Input */} +
+ + +
+ + {/* Error Message */} + {error && ( +
+ +

{error}

+
+ )} + + {/* Loading State */} + {loading && ( +
+ + Verificando... +
+ )} + + {/* Submit Button (for backup code mode) */} + {mode === 'backup' && ( + + )} + + {/* Switch Mode Link */} +
+ +
+ + {/* Info */} +
+

+ {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. + + )} +

+
+
+ + {/* Footer */} +
+ +
+
+
+ ); +}; + +export default TwoFactorVerifyModal; diff --git a/src/modules/settings/components/TwoFactorSettings.tsx b/src/modules/settings/components/TwoFactorSettings.tsx new file mode 100644 index 0000000..26228c6 --- /dev/null +++ b/src/modules/settings/components/TwoFactorSettings.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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(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 ( +
+
+
+ +
+
+

Autenticacion de Dos Factores

+

Cargando configuracion...

+
+
+
+ +
+
+ ); + } + + return ( + <> +
+
+
+
+ +
+
+

Autenticacion de Dos Factores

+

+ Agrega una capa extra de seguridad a tu cuenta +

+
+
+ {status?.enabled && ( +
+ + Activo +
+ )} +
+ + {error && ( +
+ +

{error}

+
+ )} + + {status?.enabled ? ( +
+ {/* Enabled Status */} +
+
+ +
+

2FA esta activo

+

+ Tu cuenta esta protegida con autenticacion de dos factores. + {status.enabledAt && ( + <> Activado el {new Date(status.enabledAt).toLocaleDateString()}. + )} +

+
+
+
+ + {/* Actions */} +
+ {/* Regenerate Backup Codes */} + {!showRegenerateConfirm ? ( + + ) : ( +
+
+ +
+

Confirmar regeneracion

+

+ Esto invalidara todos tus codigos de respaldo anteriores. +

+
+
+
+ + +
+
+ )} + + {/* Disable 2FA */} + {!showDisableConfirm ? ( + + ) : ( +
+
+ +
+

Confirmar desactivacion

+

+ Ingresa tu codigo TOTP actual para desactivar 2FA +

+
+
+ { + 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 + /> +
+ + +
+
+ )} +
+
+ ) : ( +
+ {/* Disabled Status */} +
+
+ +
+

2FA no esta activo

+

+ Protege tu cuenta con autenticacion de dos factores. Necesitaras una app de autenticacion como Google Authenticator o Authy. +

+
+
+
+ + {/* Enable Button */} + + + {/* Info */} +
+

+ Recomendado: La autenticacion de dos factores agrega una capa adicional de seguridad requiriendo un codigo temporal ademas de tu contrasena. +

+
+
+ )} +
+ + {/* Setup Modal */} + setShowSetup(false)} + onSuccess={handleEnableSuccess} + /> + + {/* Regenerated Codes Modal */} + {regeneratedCodes && ( +
+
+
+
+ +
+

Nuevos Codigos Generados

+

+ Guarda estos codigos en un lugar seguro. Los anteriores ya no funcionan. +

+
+
+ +
+
+ {regeneratedCodes.map((code, index) => ( +
+ {code} +
+ ))} +
+
+ +
+ + +
+
+
+
+ )} + + ); +}; + +export default TwoFactorSettings;