Se soluciono autenticacion

This commit is contained in:
2025-08-05 13:06:24 -06:00
parent c280afe646
commit c9df4e3ab2
21 changed files with 758 additions and 624 deletions

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useLayoutEffect, useRef } from 'react';
import { getCurrentUser } from '../api/users.ts';
import { useNotification } from '../context/NotificationContext';
import { fetchWithAuth, patchWithAuth, postWithAuth, postFormDataWithAuth } from '../fetchWithAuth';
// Animación fade-in/slide-up para el componente
const fadeInSlideUp = `@keyframes fadein-slideup { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; transform: translateY(0); } }`;
@@ -27,6 +28,16 @@ const Settings = () => {
rfc: ''
});
// Estados para cambio de contraseña
const [passwordData, setPasswordData] = useState({
current_password: '',
new_password: '',
confirm_password: ''
});
const [passwordErrors, setPasswordErrors] = useState({});
const [changingPassword, setChangingPassword] = useState(false);
// Estado para controlar la animación de entrada
const [showAnimation, setShowAnimation] = useState(false);
const [hasAnimated, setHasAnimated] = useState(false);
@@ -79,28 +90,10 @@ const Settings = () => {
// Función para actualizar el usuario
const updateUser = async (userData) => {
const token = localStorage.getItem('access');
const API_URL = import.meta.env.VITE_EFC_API_URL;
try {
const response = await fetch(`${API_URL}/user/users/${currentUser.id}/`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
if (response.status === 401) {
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
localStorage.removeItem('access');
localStorage.removeItem('refresh');
setTimeout(() => {
window.location.href = '/login';
}, 2000);
return null;
}
const response = await patchWithAuth(`${API_URL}/user/users/${currentUser.id}/`, userData);
if (!response.ok) {
const errorData = await response.json();
@@ -123,6 +116,105 @@ const Settings = () => {
}));
};
// Manejar cambios en el formulario de contraseña
const handlePasswordChange = (e) => {
const { name, value } = e.target;
setPasswordData(prev => ({
...prev,
[name]: value
}));
// Limpiar errores cuando el usuario empiece a escribir
if (passwordErrors[name]) {
setPasswordErrors(prev => ({
...prev,
[name]: ''
}));
}
};
// Validar contraseña
const validatePassword = (password) => {
const errors = [];
if (password.length < 8) {
errors.push('Debe tener al menos 8 caracteres');
}
if (!/(?=.*[a-z])/.test(password)) {
errors.push('Debe contener al menos una letra minúscula');
}
if (!/(?=.*[A-Z])/.test(password)) {
errors.push('Debe contener al menos una letra mayúscula');
}
if (!/(?=.*\d)/.test(password)) {
errors.push('Debe contener al menos un número');
}
return errors;
};
// Cambiar contraseña
const handleChangePassword = async () => {
const errors = {};
// Validaciones
if (!passwordData.current_password.trim()) {
errors.current_password = 'La contraseña actual es requerida';
}
if (!passwordData.new_password.trim()) {
errors.new_password = 'La nueva contraseña es requerida';
} else {
const passwordValidationErrors = validatePassword(passwordData.new_password);
if (passwordValidationErrors.length > 0) {
errors.new_password = passwordValidationErrors[0]; // Mostrar el primer error
}
}
if (!passwordData.confirm_password.trim()) {
errors.confirm_password = 'Confirma tu nueva contraseña';
} else if (passwordData.new_password !== passwordData.confirm_password) {
errors.confirm_password = 'Las contraseñas no coinciden';
}
if (Object.keys(errors).length > 0) {
setPasswordErrors(errors);
return;
}
setChangingPassword(true);
try {
const API_URL = import.meta.env.VITE_EFC_API_URL;
const response = await postWithAuth(`${API_URL}/user/users/${currentUser.id}/change_password/`, {
old_password: passwordData.current_password,
new_password: passwordData.new_password
});
if (!response.ok) {
const errorData = await response.json();
if (errorData.old_password) {
setPasswordErrors({ current_password: 'La contraseña actual es incorrecta' });
} else {
throw new Error(errorData.detail || 'Error al cambiar la contraseña');
}
return;
}
// Limpiar formulario y mostrar mensaje de éxito
setPasswordData({
current_password: '',
new_password: '',
confirm_password: ''
});
setPasswordErrors({});
showMessage('Contraseña cambiada exitosamente', 'success');
} catch (error) {
console.error('Error changing password:', error);
showMessage(error.message || 'Error al cambiar la contraseña', 'error');
} finally {
setChangingPassword(false);
}
};
// Guardar cambios del perfil
const handleSaveProfile = async () => {
if (!currentUser) return;
@@ -642,9 +734,19 @@ const Settings = () => {
</label>
<input
type="password"
className="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-all duration-200 hover:border-gray-400"
name="current_password"
value={passwordData.current_password}
onChange={handlePasswordChange}
className={`w-full px-4 py-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2 text-sm transition-all duration-200 hover:border-gray-400 ${
passwordErrors.current_password
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
}`}
placeholder="Ingresa tu contraseña actual"
/>
{passwordErrors.current_password && (
<p className="mt-1 text-sm text-red-600">{passwordErrors.current_password}</p>
)}
</div>
<div className="space-y-2">
@@ -653,9 +755,19 @@ const Settings = () => {
</label>
<input
type="password"
className="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-all duration-200 hover:border-gray-400"
name="new_password"
value={passwordData.new_password}
onChange={handlePasswordChange}
className={`w-full px-4 py-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2 text-sm transition-all duration-200 hover:border-gray-400 ${
passwordErrors.new_password
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
}`}
placeholder="Ingresa una nueva contraseña"
/>
{passwordErrors.new_password && (
<p className="mt-1 text-sm text-red-600">{passwordErrors.new_password}</p>
)}
</div>
<div className="space-y-2">
@@ -664,28 +776,52 @@ const Settings = () => {
</label>
<input
type="password"
className="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-all duration-200 hover:border-gray-400"
name="confirm_password"
value={passwordData.confirm_password}
onChange={handlePasswordChange}
className={`w-full px-4 py-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2 text-sm transition-all duration-200 hover:border-gray-400 ${
passwordErrors.confirm_password
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
}`}
placeholder="Confirma tu nueva contraseña"
/>
{passwordErrors.confirm_password && (
<p className="mt-1 text-sm text-red-600">{passwordErrors.confirm_password}</p>
)}
</div>
</div>
<div className="mt-6">
<button
type="button"
className="inline-flex items-center px-6 py-3 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"
onClick={handleChangePassword}
disabled={changingPassword}
className="inline-flex items-center px-6 py-3 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Actualizar contraseña
{changingPassword ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Cambiando contraseña...
</>
) : (
<>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Actualizar contraseña
</>
)}
</button>
</div>
</div>
{/* Información de seguridad adicional */}
<div className="bg-gray-50 rounded-xl p-6">
<h4 className="text-lg font-semibold text-gray-900 mb-4">Consejos de seguridad</h4>
<h4 className="text-lg font-semibold text-gray-900 mb-4">Requisitos de contraseña</h4>
<div className="space-y-3">
<div className="flex items-start">
<div className="flex-shrink-0">
@@ -694,7 +830,7 @@ const Settings = () => {
</svg>
</div>
<p className="ml-3 text-sm text-gray-600">
Usa al menos 8 caracteres con una combinación de letras, números y símbolos
Mínimo 8 caracteres de longitud
</p>
</div>
<div className="flex items-start">
@@ -704,7 +840,7 @@ const Settings = () => {
</svg>
</div>
<p className="ml-3 text-sm text-gray-600">
Evita usar información personal como nombres o fechas de nacimiento
Al menos una letra minúscula (a-z)
</p>
</div>
<div className="flex items-start">
@@ -714,9 +850,55 @@ const Settings = () => {
</svg>
</div>
<p className="ml-3 text-sm text-gray-600">
No reutilices contraseñas de otras cuentas
Al menos una letra mayúscula (A-Z)
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-green-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<p className="ml-3 text-sm text-gray-600">
Al menos un número (0-9)
</p>
</div>
</div>
<div className="mt-6 pt-4 border-t border-gray-200">
<h5 className="text-sm font-semibold text-gray-900 mb-2">Consejos adicionales</h5>
<div className="space-y-2">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-4 h-4 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="ml-2 text-xs text-gray-600">
Evita usar información personal como nombres o fechas de nacimiento
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-4 h-4 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="ml-2 text-xs text-gray-600">
No reutilices contraseñas de otras cuentas
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-4 h-4 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="ml-2 text-xs text-gray-600">
Considera usar símbolos especiales para mayor seguridad
</p>
</div>
</div>
</div>
</div>
</div>