feat: enhance authentication system with token-based login and session management
- Updated the backend to support token-based authentication, replacing basic auth. - Added session management functions for creating, invalidating, and refreshing sessions. - Refactored user routes to include login and logout endpoints. - Modified frontend to handle token storage and session validation. - Improved user experience by ensuring sessions are invalidated upon password changes.
This commit is contained in:
@@ -428,37 +428,21 @@ async function handleGlobalLogin() {
|
||||
}
|
||||
|
||||
try {
|
||||
if (globalLoginForm.value.remember) {
|
||||
authService.saveCredentials(
|
||||
globalLoginForm.value.username,
|
||||
globalLoginForm.value.password
|
||||
);
|
||||
}
|
||||
// Usar el nuevo método login que genera un token
|
||||
await authService.login(
|
||||
globalLoginForm.value.username,
|
||||
globalLoginForm.value.password
|
||||
);
|
||||
|
||||
// Intentar hacer una petición autenticada para validar credenciales
|
||||
// Usamos stats que no requiere auth, pero validará las credenciales
|
||||
try {
|
||||
await api.getStats();
|
||||
} catch (error) {
|
||||
// Si hay error 401, las credenciales son inválidas
|
||||
if (error.response?.status === 401) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Si llegamos aquí, las credenciales son válidas
|
||||
// Si llegamos aquí, el login fue exitoso y el token está guardado
|
||||
closeLoginModal();
|
||||
|
||||
// Recargar página para actualizar datos después del login
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('Error en login:', error);
|
||||
if (error.response?.status === 401) {
|
||||
globalLoginError.value = 'Usuario o contraseña incorrectos';
|
||||
authService.clearCredentials();
|
||||
} else {
|
||||
globalLoginError.value = 'Error de conexión. Intenta de nuevo.';
|
||||
}
|
||||
globalLoginError.value = error.message || 'Usuario o contraseña incorrectos';
|
||||
authService.clearSession();
|
||||
} finally {
|
||||
globalLoginLoading.value = false;
|
||||
}
|
||||
@@ -481,9 +465,9 @@ function handleAuthRequired(event) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
// Limpiar credenciales
|
||||
authService.clearCredentials();
|
||||
async function handleLogout() {
|
||||
// Llamar al endpoint de logout e invalidar token
|
||||
await authService.logout();
|
||||
|
||||
// Redirigir al dashboard después del logout
|
||||
router.push('/');
|
||||
@@ -500,11 +484,19 @@ onMounted(async () => {
|
||||
connectWebSocket();
|
||||
await checkPushStatus();
|
||||
|
||||
// Cargar credenciales guardadas si existen
|
||||
// Cargar username guardado si existe (pero no la contraseña)
|
||||
if (authService.hasCredentials()) {
|
||||
const creds = authService.getCredentials();
|
||||
globalLoginForm.value.username = creds.username;
|
||||
globalLoginForm.value.password = creds.password;
|
||||
const username = authService.getUsername();
|
||||
if (username) {
|
||||
globalLoginForm.value.username = username;
|
||||
}
|
||||
|
||||
// Validar si el token sigue siendo válido
|
||||
const isValid = await authService.validateSession();
|
||||
if (!isValid) {
|
||||
// Si el token expiró, limpiar sesión
|
||||
authService.clearSession();
|
||||
}
|
||||
}
|
||||
|
||||
// Escuchar eventos de autenticación requerida
|
||||
|
||||
@@ -1,89 +1,172 @@
|
||||
// Servicio de autenticación para gestionar credenciales
|
||||
// Servicio de autenticación para gestionar tokens
|
||||
|
||||
const AUTH_STORAGE_KEY = 'wallabicher_auth';
|
||||
const AUTH_STORAGE_KEY = 'wallabicher_token';
|
||||
const USERNAME_STORAGE_KEY = 'wallabicher_username';
|
||||
|
||||
class AuthService {
|
||||
constructor() {
|
||||
this.credentials = this.loadCredentials();
|
||||
this.token = this.loadToken();
|
||||
this.username = this.loadUsername();
|
||||
}
|
||||
|
||||
// Cargar credenciales desde localStorage
|
||||
loadCredentials() {
|
||||
// Cargar token desde localStorage
|
||||
loadToken() {
|
||||
try {
|
||||
const stored = localStorage.getItem(AUTH_STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
return {
|
||||
username: parsed.username || '',
|
||||
password: parsed.password || '',
|
||||
};
|
||||
}
|
||||
return localStorage.getItem(AUTH_STORAGE_KEY) || '';
|
||||
} catch (error) {
|
||||
console.error('Error cargando credenciales:', error);
|
||||
console.error('Error cargando token:', error);
|
||||
return '';
|
||||
}
|
||||
return { username: '', password: '' };
|
||||
}
|
||||
|
||||
// Guardar credenciales en localStorage
|
||||
saveCredentials(username, password) {
|
||||
// Cargar username desde localStorage
|
||||
loadUsername() {
|
||||
try {
|
||||
this.credentials = { username, password };
|
||||
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(this.credentials));
|
||||
return localStorage.getItem(USERNAME_STORAGE_KEY) || '';
|
||||
} catch (error) {
|
||||
console.error('Error cargando username:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Guardar token y username en localStorage
|
||||
saveSession(token, username) {
|
||||
try {
|
||||
this.token = token;
|
||||
this.username = username;
|
||||
localStorage.setItem(AUTH_STORAGE_KEY, token);
|
||||
localStorage.setItem(USERNAME_STORAGE_KEY, username);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error guardando credenciales:', error);
|
||||
console.error('Error guardando sesión:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Eliminar credenciales
|
||||
clearCredentials() {
|
||||
// Eliminar token y username
|
||||
clearSession() {
|
||||
try {
|
||||
this.credentials = { username: '', password: '' };
|
||||
this.token = '';
|
||||
this.username = '';
|
||||
localStorage.removeItem(AUTH_STORAGE_KEY);
|
||||
localStorage.removeItem(USERNAME_STORAGE_KEY);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error eliminando credenciales:', error);
|
||||
console.error('Error eliminando sesión:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Obtener credenciales actuales
|
||||
getCredentials() {
|
||||
return { ...this.credentials };
|
||||
// Obtener token actual
|
||||
getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
// Verificar si hay credenciales guardadas
|
||||
// Obtener username actual
|
||||
getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
// Verificar si hay sesión activa (token guardado)
|
||||
hasCredentials() {
|
||||
return !!(this.credentials.username && this.credentials.password);
|
||||
return !!this.token;
|
||||
}
|
||||
|
||||
// Generar header de autenticación Basic
|
||||
// Generar header de autenticación Bearer
|
||||
getAuthHeader() {
|
||||
if (!this.hasCredentials()) {
|
||||
if (!this.token) {
|
||||
return null;
|
||||
}
|
||||
const { username, password } = this.credentials;
|
||||
const encoded = btoa(`${username}:${password}`);
|
||||
return `Basic ${encoded}`;
|
||||
return `Bearer ${this.token}`;
|
||||
}
|
||||
|
||||
// Validar credenciales (test básico)
|
||||
async validateCredentials(username, password) {
|
||||
// Hacer login (llamar al endpoint de login)
|
||||
async login(username, password) {
|
||||
try {
|
||||
// Intentar hacer una petición simple para validar las credenciales
|
||||
const encoded = btoa(`${username}:${password}`);
|
||||
const response = await fetch('/api/stats', {
|
||||
const response = await fetch('/api/users/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Error en login');
|
||||
}
|
||||
|
||||
if (data.success && data.token) {
|
||||
this.saveSession(data.token, data.username);
|
||||
return { success: true, token: data.token, username: data.username };
|
||||
}
|
||||
|
||||
throw new Error('Respuesta inválida del servidor');
|
||||
} catch (error) {
|
||||
console.error('Error en login:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Hacer logout (llamar al endpoint de logout)
|
||||
async logout() {
|
||||
try {
|
||||
const token = this.token;
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
await fetch('/api/users/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Si falla el logout en el servidor, aún así limpiar localmente
|
||||
console.error('Error al cerrar sesión en el servidor:', error);
|
||||
}
|
||||
}
|
||||
|
||||
this.clearSession();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error en logout:', error);
|
||||
this.clearSession(); // Limpiar localmente de todas formas
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar si el token sigue siendo válido
|
||||
async validateSession() {
|
||||
if (!this.token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users/me', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Basic ${encoded}`,
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Si la petición funciona, las credenciales son válidas
|
||||
// Nota: stats no requiere auth, pero podemos usar cualquier endpoint
|
||||
return response.ok || response.status !== 401;
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.authenticated) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Si el token es inválido, limpiar sesión
|
||||
if (response.status === 401) {
|
||||
this.clearSession();
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error validando sesión:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,8 +351,7 @@ const passwordForm = ref({
|
||||
|
||||
const isAuthenticated = computed(() => authService.hasCredentials());
|
||||
const currentUser = computed(() => {
|
||||
const creds = authService.getCredentials();
|
||||
return creds.username || '';
|
||||
return authService.getUsername() || '';
|
||||
});
|
||||
|
||||
function formatDate(dateString) {
|
||||
@@ -461,17 +460,16 @@ async function handleChangePassword() {
|
||||
newPassword: passwordForm.value.newPassword,
|
||||
});
|
||||
|
||||
passwordSuccess.value = 'Contraseña actualizada correctamente';
|
||||
passwordSuccess.value = 'Contraseña actualizada correctamente. Por favor, inicia sesión nuevamente.';
|
||||
|
||||
// Actualizar credenciales guardadas si la nueva contraseña es para el usuario actual
|
||||
const creds = authService.getCredentials();
|
||||
if (creds.username === currentUser.value) {
|
||||
authService.saveCredentials(currentUser.value, passwordForm.value.newPassword);
|
||||
}
|
||||
|
||||
// Limpiar formulario después de 2 segundos
|
||||
setTimeout(() => {
|
||||
// Invalidar la sesión actual - el usuario deberá hacer login nuevamente
|
||||
// El backend ya invalidó todas las sesiones, así que limpiamos localmente también
|
||||
setTimeout(async () => {
|
||||
await authService.logout();
|
||||
closeChangePasswordModal();
|
||||
// Recargar página para forzar nuevo login
|
||||
// El evento auth-required se disparará automáticamente cuando intente cargar datos
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Error cambiando contraseña:', error);
|
||||
|
||||
Reference in New Issue
Block a user