325 lines
11 KiB
TypeScript
325 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useAuthStore } from '@/lib/auth/client/useAuthStore';
|
|
import { apiClient } from '@/lib/utils/api-client';
|
|
import { logInfo, logError } from '@/lib/logger/client-logger';
|
|
|
|
type LoginStep = 'credentials' | 'twoFactor';
|
|
|
|
export default function LoginForm() {
|
|
const [step, setStep] = useState<LoginStep>('credentials');
|
|
const [userName, setUserName] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [softToken, setSoftToken] = useState('');
|
|
const [error, setError] = useState('');
|
|
|
|
const [userNameError, setUserNameError] = useState('');
|
|
const [passwordError, setPasswordError] = useState('');
|
|
const [tokenError, setTokenError] = useState('');
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showDeactivateToken, setShowDeactivateToken] = useState(false);
|
|
|
|
const router = useRouter();
|
|
const { setUser, setRequires2FA, setLoading } = useAuthStore();
|
|
|
|
useEffect(() => {
|
|
if (userName.length > 12) {
|
|
setUserName(userName.substring(0, 12));
|
|
}
|
|
}, [userName]);
|
|
|
|
const validateUserName = (value: string): string => {
|
|
if (!value || value.trim() === '') {
|
|
return 'Este campo es obligatorio';
|
|
}
|
|
if (value.length < 6) {
|
|
return 'Debe ser mayor o igual a seis caracteres';
|
|
}
|
|
return '';
|
|
};
|
|
|
|
const validatePassword = (value: string): string =>
|
|
value?.trim() ? '' : 'Este campo es obligatorio';
|
|
|
|
const validateToken = (value: string): string =>
|
|
value?.trim() ? '' : 'Este campo es obligatorio';
|
|
|
|
const handleUserNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value;
|
|
setUserName(value);
|
|
setUserNameError(validateUserName(value));
|
|
setError('');
|
|
};
|
|
|
|
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value;
|
|
setPassword(value);
|
|
setPasswordError(validatePassword(value));
|
|
setError('');
|
|
};
|
|
|
|
const handleTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value;
|
|
setSoftToken(value);
|
|
setTokenError(validateToken(value));
|
|
setError('');
|
|
};
|
|
|
|
const handleCredentialsSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
const userNameErr = validateUserName(userName);
|
|
const passwordErr = validatePassword(password);
|
|
|
|
setUserNameError(userNameErr);
|
|
setPasswordError(passwordErr);
|
|
|
|
if (userNameErr || passwordErr) {
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
setLoading(true);
|
|
|
|
try {
|
|
const response = await apiClient.post<any>('/api/auth/login', { userName, password });
|
|
|
|
if (response.error) {
|
|
throw new Error(response.error);
|
|
}
|
|
|
|
if (response.requires2FA) {
|
|
setRequires2FA(true, userName);
|
|
setStep('twoFactor');
|
|
setShowDeactivateToken(true);
|
|
} else {
|
|
setUser(response.user);
|
|
logInfo('Login exitoso de credenciales', { userId: response.user.id });
|
|
router.push('/dashboard');
|
|
router.refresh();
|
|
}
|
|
} catch (err: any) {
|
|
logError('Error en credenciales de login', { error: err.message });
|
|
setError(err.message || 'Error desconocido.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleTwoFactorSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
const tokenErr = validateToken(softToken);
|
|
setTokenError(tokenErr);
|
|
|
|
if (tokenErr) {
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
setLoading(true);
|
|
|
|
try {
|
|
const response = await apiClient.post<any>('/api/auth/login', { userName, password, softToken });
|
|
|
|
if (response.error) {
|
|
throw new Error(response.error);
|
|
}
|
|
|
|
setUser(response.user);
|
|
logInfo('Login exitoso con 2FA completado', { userId: response.user.id });
|
|
router.push('/dashboard');
|
|
router.refresh();
|
|
} catch (err: any) {
|
|
logError('Fallo de validacion de token 2FA', { error: err.message });
|
|
setError(err.message || 'Código de autenticación inválido.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const isCredentialsButtonDisabled = !userName || !password || isLoading || !!userNameError || !!passwordError;
|
|
const isTokenButtonDisabled = !softToken || isLoading || !!tokenError;
|
|
|
|
return (
|
|
<form
|
|
onSubmit={step === 'credentials' ? handleCredentialsSubmit : handleTwoFactorSubmit}
|
|
className="space-y-6"
|
|
>
|
|
{error && (
|
|
<div className="bg-error/10 border border-error rounded-lg p-4 flex items-start gap-3">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="h-5 w-5 text-error flex-shrink-0 mt-0.5"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
>
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
<span className="text-error text-sm font-medium">{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
{step === 'credentials' ? (
|
|
<>
|
|
<div className="form-control w-full">
|
|
<label className="label">
|
|
<span className="label-text font-semibold">Usuario</span>
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
placeholder="Ingresa tu usuario"
|
|
value={userName}
|
|
onChange={handleUserNameChange}
|
|
required
|
|
maxLength={12}
|
|
className={`input input-bordered w-full pl-10 ${
|
|
userNameError ? 'input-error' : ''
|
|
}`}
|
|
/>
|
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-base-content/50">
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
</span>
|
|
</div>
|
|
{userNameError && (
|
|
<label className="label">
|
|
<span className="label-text-alt text-error">{userNameError}</span>
|
|
</label>
|
|
)}
|
|
</div>
|
|
|
|
<div className="form-control w-full">
|
|
<label className="label">
|
|
<span className="label-text font-semibold">Clave</span>
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showPassword ? 'text' : 'password'}
|
|
placeholder="Ingresa tu contraseña"
|
|
value={password}
|
|
onChange={handlePasswordChange}
|
|
required
|
|
className={`input input-bordered w-full pl-10 pr-10 ${
|
|
passwordError ? 'input-error' : ''
|
|
}`}
|
|
/>
|
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-base-content/50">
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/50 hover:text-base-content"
|
|
>
|
|
{showPassword ? (
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
) : (
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l18 18" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
{passwordError && (
|
|
<label className="label">
|
|
<span className="label-text-alt text-error">{passwordError}</span>
|
|
</label>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
className="btn btn-primary w-full text-white"
|
|
disabled={isCredentialsButtonDisabled}
|
|
>
|
|
{isLoading ? 'Ingresando...' : 'Ingresar'}
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="text-center space-y-2">
|
|
<h3 className="text-lg font-bold">Autenticación de dos factores (2FA)</h3>
|
|
<p className="text-sm text-base-content/70">
|
|
Ingresa el código numérico de 6 dígitos de tu aplicación móvil Soft Token.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="form-control w-full">
|
|
<label className="label">
|
|
<span className="label-text font-semibold">Código OTP</span>
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
placeholder="000000"
|
|
value={softToken}
|
|
onChange={handleTokenChange}
|
|
required
|
|
maxLength={6}
|
|
className={`input input-bordered w-full text-center tracking-widest text-lg font-mono ${
|
|
tokenError ? 'input-error' : ''
|
|
}`}
|
|
/>
|
|
</div>
|
|
{tokenError && (
|
|
<label className="label">
|
|
<span className="label-text-alt text-error">{tokenError}</span>
|
|
</label>
|
|
)}
|
|
</div>
|
|
|
|
{showDeactivateToken && (
|
|
<div className="text-center">
|
|
<p className="text-xs text-base-content/60">
|
|
¿No tienes acceso a tu móvil? Ingresa en Autogestión.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-3">
|
|
<button
|
|
type="submit"
|
|
className="btn btn-primary w-full text-white"
|
|
disabled={isTokenButtonDisabled}
|
|
>
|
|
{isLoading ? 'Verificando...' : 'Verificar código'}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="btn btn-outline w-full"
|
|
onClick={() => {
|
|
setStep('credentials');
|
|
setSoftToken('');
|
|
setTokenError('');
|
|
setError('');
|
|
}}
|
|
>
|
|
Volver
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</form>
|
|
);
|
|
}
|