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:
@@ -13,6 +13,11 @@ RUN npm ci --only=production
|
|||||||
|
|
||||||
# Copiar código de la aplicación
|
# Copiar código de la aplicación
|
||||||
COPY server.js .
|
COPY server.js .
|
||||||
|
COPY config/ ./config/
|
||||||
|
COPY middlewares/ ./middlewares/
|
||||||
|
COPY routes/ ./routes/
|
||||||
|
COPY services/ ./services/
|
||||||
|
COPY utils/ ./utils/
|
||||||
|
|
||||||
# Exponer puerto
|
# Exponer puerto
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import bcrypt from 'bcrypt';
|
import crypto from 'crypto';
|
||||||
import { getRedisClient } from '../services/redis.js';
|
import { getRedisClient } from '../services/redis.js';
|
||||||
|
|
||||||
// Autenticación básica Middleware
|
// Duración de la sesión en segundos (24 horas)
|
||||||
export async function basicAuthMiddleware(req, res, next) {
|
const SESSION_DURATION = 24 * 60 * 60;
|
||||||
|
|
||||||
|
// Generar token seguro
|
||||||
|
function generateToken() {
|
||||||
|
return crypto.randomBytes(32).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Autenticación por token Middleware
|
||||||
|
export async function authMiddleware(req, res, next) {
|
||||||
const redisClient = getRedisClient();
|
const redisClient = getRedisClient();
|
||||||
|
|
||||||
if (!redisClient) {
|
if (!redisClient) {
|
||||||
@@ -12,50 +20,43 @@ export async function basicAuthMiddleware(req, res, next) {
|
|||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
if (!authHeader) {
|
if (!authHeader) {
|
||||||
// NO enviar WWW-Authenticate para evitar el diálogo nativo del navegador
|
|
||||||
// En su lugar, devolveremos un 401 y el frontend manejará el modal personalizado
|
|
||||||
return res.status(401).json({ error: 'Authentication required', message: 'Se requiere autenticación para esta operación' });
|
return res.status(401).json({ error: 'Authentication required', message: 'Se requiere autenticación para esta operación' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const [scheme, encoded] = authHeader.split(' ');
|
const [scheme, token] = authHeader.split(' ');
|
||||||
|
|
||||||
if (scheme !== 'Basic') {
|
if (scheme !== 'Bearer' || !token) {
|
||||||
return res.status(400).json({ error: 'Bad request', message: 'Solo se admite autenticación Basic' });
|
return res.status(400).json({ error: 'Bad request', message: 'Se requiere un token Bearer' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buffer = Buffer.from(encoded, 'base64');
|
// Verificar token en Redis
|
||||||
const [username, password] = buffer.toString().split(':');
|
const sessionKey = `session:${token}`;
|
||||||
|
const sessionData = await redisClient.get(sessionKey);
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!sessionData) {
|
||||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña no proporcionados' });
|
return res.status(401).json({ error: 'Invalid token', message: 'Token inválido o sesión expirada' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar usuario en Redis
|
// Parsear datos de sesión
|
||||||
const userKey = `user:${username}`;
|
const session = JSON.parse(sessionData);
|
||||||
|
|
||||||
|
// Verificar que el usuario aún existe
|
||||||
|
const userKey = `user:${session.username}`;
|
||||||
const userExists = await redisClient.exists(userKey);
|
const userExists = await redisClient.exists(userKey);
|
||||||
|
|
||||||
if (!userExists) {
|
if (!userExists) {
|
||||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
// Eliminar sesión si el usuario ya no existe
|
||||||
|
await redisClient.del(sessionKey);
|
||||||
|
return res.status(401).json({ error: 'Invalid token', message: 'Usuario no encontrado' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtener hash de la contraseña
|
// Actualizar TTL de la sesión (refresh)
|
||||||
const userData = await redisClient.hGetAll(userKey);
|
await redisClient.expire(sessionKey, SESSION_DURATION);
|
||||||
const passwordHash = userData.passwordHash;
|
|
||||||
|
|
||||||
if (!passwordHash) {
|
|
||||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar contraseña
|
|
||||||
const match = await bcrypt.compare(password, passwordHash);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Autenticación exitosa
|
// Autenticación exitosa
|
||||||
req.user = { username };
|
req.user = { username: session.username };
|
||||||
|
req.token = token;
|
||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error en autenticación:', error);
|
console.error('Error en autenticación:', error);
|
||||||
@@ -63,3 +64,73 @@ export async function basicAuthMiddleware(req, res, next) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alias para mantener compatibilidad
|
||||||
|
export const basicAuthMiddleware = authMiddleware;
|
||||||
|
|
||||||
|
// Función para crear sesión
|
||||||
|
export async function createSession(username) {
|
||||||
|
const redisClient = getRedisClient();
|
||||||
|
if (!redisClient) {
|
||||||
|
throw new Error('Redis no está disponible');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = generateToken();
|
||||||
|
const sessionKey = `session:${token}`;
|
||||||
|
const sessionData = {
|
||||||
|
username,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Almacenar sesión en Redis con TTL
|
||||||
|
await redisClient.setEx(sessionKey, SESSION_DURATION, JSON.stringify(sessionData));
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para invalidar sesión
|
||||||
|
export async function invalidateSession(token) {
|
||||||
|
const redisClient = getRedisClient();
|
||||||
|
if (!redisClient) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionKey = `session:${token}`;
|
||||||
|
await redisClient.del(sessionKey);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error invalidando sesión:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para invalidar todas las sesiones de un usuario
|
||||||
|
export async function invalidateUserSessions(username) {
|
||||||
|
const redisClient = getRedisClient();
|
||||||
|
if (!redisClient) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Buscar todas las sesiones del usuario
|
||||||
|
const keys = await redisClient.keys('session:*');
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const sessionData = await redisClient.get(key);
|
||||||
|
if (sessionData) {
|
||||||
|
const session = JSON.parse(sessionData);
|
||||||
|
if (session.username === username) {
|
||||||
|
await redisClient.del(key);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error invalidando sesiones del usuario:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,93 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import { getRedisClient } from '../services/redis.js';
|
import { getRedisClient } from '../services/redis.js';
|
||||||
import { basicAuthMiddleware } from '../middlewares/auth.js';
|
import { basicAuthMiddleware, createSession, invalidateSession, invalidateUserSessions } from '../middlewares/auth.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Endpoint de login (público)
|
||||||
|
router.post('/login', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const redisClient = getRedisClient();
|
||||||
|
if (!redisClient) {
|
||||||
|
return res.status(500).json({ error: 'Redis no está disponible' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({ error: 'username y password son requeridos' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar usuario en Redis
|
||||||
|
const userKey = `user:${username}`;
|
||||||
|
const userExists = await redisClient.exists(userKey);
|
||||||
|
|
||||||
|
if (!userExists) {
|
||||||
|
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener hash de la contraseña
|
||||||
|
const userData = await redisClient.hGetAll(userKey);
|
||||||
|
const passwordHash = userData.passwordHash;
|
||||||
|
|
||||||
|
if (!passwordHash) {
|
||||||
|
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar contraseña
|
||||||
|
const match = await bcrypt.compare(password, passwordHash);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear sesión/token
|
||||||
|
const token = await createSession(username);
|
||||||
|
|
||||||
|
console.log(`✅ Login exitoso: ${username}`);
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
token,
|
||||||
|
username,
|
||||||
|
message: 'Login exitoso'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error en login:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Endpoint de logout (requiere autenticación)
|
||||||
|
router.post('/logout', basicAuthMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const token = req.token;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
await invalidateSession(token);
|
||||||
|
console.log(`✅ Logout exitoso: ${req.user.username}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Logout exitoso' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error en logout:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verificar token (para validar si la sesión sigue activa)
|
||||||
|
router.get('/me', basicAuthMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
username: req.user.username,
|
||||||
|
authenticated: true
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Cambiar contraseña de usuario
|
// Cambiar contraseña de usuario
|
||||||
router.post('/change-password', basicAuthMiddleware, async (req, res) => {
|
router.post('/change-password', basicAuthMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -45,8 +128,11 @@ router.post('/change-password', basicAuthMiddleware, async (req, res) => {
|
|||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`✅ Contraseña actualizada para usuario: ${username}`);
|
// Invalidar todas las sesiones del usuario (requiere nuevo login)
|
||||||
res.json({ success: true, message: 'Contraseña actualizada correctamente' });
|
await invalidateUserSessions(username);
|
||||||
|
|
||||||
|
console.log(`✅ Contraseña actualizada para usuario: ${username} (todas las sesiones invalidadas)`);
|
||||||
|
res.json({ success: true, message: 'Contraseña actualizada correctamente. Por favor, inicia sesión nuevamente.' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error cambiando contraseña:', error);
|
console.error('Error cambiando contraseña:', error);
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
|
|||||||
@@ -428,37 +428,21 @@ async function handleGlobalLogin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (globalLoginForm.value.remember) {
|
// Usar el nuevo método login que genera un token
|
||||||
authService.saveCredentials(
|
await authService.login(
|
||||||
globalLoginForm.value.username,
|
globalLoginForm.value.username,
|
||||||
globalLoginForm.value.password
|
globalLoginForm.value.password
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Intentar hacer una petición autenticada para validar credenciales
|
// Si llegamos aquí, el login fue exitoso y el token está guardado
|
||||||
// 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
|
|
||||||
closeLoginModal();
|
closeLoginModal();
|
||||||
|
|
||||||
// Recargar página para actualizar datos después del login
|
// Recargar página para actualizar datos después del login
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error en login:', error);
|
console.error('Error en login:', error);
|
||||||
if (error.response?.status === 401) {
|
globalLoginError.value = error.message || 'Usuario o contraseña incorrectos';
|
||||||
globalLoginError.value = 'Usuario o contraseña incorrectos';
|
authService.clearSession();
|
||||||
authService.clearCredentials();
|
|
||||||
} else {
|
|
||||||
globalLoginError.value = 'Error de conexión. Intenta de nuevo.';
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
globalLoginLoading.value = false;
|
globalLoginLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -481,9 +465,9 @@ function handleAuthRequired(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLogout() {
|
async function handleLogout() {
|
||||||
// Limpiar credenciales
|
// Llamar al endpoint de logout e invalidar token
|
||||||
authService.clearCredentials();
|
await authService.logout();
|
||||||
|
|
||||||
// Redirigir al dashboard después del logout
|
// Redirigir al dashboard después del logout
|
||||||
router.push('/');
|
router.push('/');
|
||||||
@@ -500,11 +484,19 @@ onMounted(async () => {
|
|||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
await checkPushStatus();
|
await checkPushStatus();
|
||||||
|
|
||||||
// Cargar credenciales guardadas si existen
|
// Cargar username guardado si existe (pero no la contraseña)
|
||||||
if (authService.hasCredentials()) {
|
if (authService.hasCredentials()) {
|
||||||
const creds = authService.getCredentials();
|
const username = authService.getUsername();
|
||||||
globalLoginForm.value.username = creds.username;
|
if (username) {
|
||||||
globalLoginForm.value.password = creds.password;
|
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
|
// 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 {
|
class AuthService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.credentials = this.loadCredentials();
|
this.token = this.loadToken();
|
||||||
|
this.username = this.loadUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cargar credenciales desde localStorage
|
// Cargar token desde localStorage
|
||||||
loadCredentials() {
|
loadToken() {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(AUTH_STORAGE_KEY);
|
return localStorage.getItem(AUTH_STORAGE_KEY) || '';
|
||||||
if (stored) {
|
|
||||||
const parsed = JSON.parse(stored);
|
|
||||||
return {
|
|
||||||
username: parsed.username || '',
|
|
||||||
password: parsed.password || '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error cargando credenciales:', error);
|
console.error('Error cargando token:', error);
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
return { username: '', password: '' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guardar credenciales en localStorage
|
// Cargar username desde localStorage
|
||||||
saveCredentials(username, password) {
|
loadUsername() {
|
||||||
try {
|
try {
|
||||||
this.credentials = { username, password };
|
return localStorage.getItem(USERNAME_STORAGE_KEY) || '';
|
||||||
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(this.credentials));
|
} 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;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error guardando credenciales:', error);
|
console.error('Error guardando sesión:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eliminar credenciales
|
// Eliminar token y username
|
||||||
clearCredentials() {
|
clearSession() {
|
||||||
try {
|
try {
|
||||||
this.credentials = { username: '', password: '' };
|
this.token = '';
|
||||||
|
this.username = '';
|
||||||
localStorage.removeItem(AUTH_STORAGE_KEY);
|
localStorage.removeItem(AUTH_STORAGE_KEY);
|
||||||
|
localStorage.removeItem(USERNAME_STORAGE_KEY);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error eliminando credenciales:', error);
|
console.error('Error eliminando sesión:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtener credenciales actuales
|
// Obtener token actual
|
||||||
getCredentials() {
|
getToken() {
|
||||||
return { ...this.credentials };
|
return this.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar si hay credenciales guardadas
|
// Obtener username actual
|
||||||
|
getUsername() {
|
||||||
|
return this.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si hay sesión activa (token guardado)
|
||||||
hasCredentials() {
|
hasCredentials() {
|
||||||
return !!(this.credentials.username && this.credentials.password);
|
return !!this.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generar header de autenticación Basic
|
// Generar header de autenticación Bearer
|
||||||
getAuthHeader() {
|
getAuthHeader() {
|
||||||
if (!this.hasCredentials()) {
|
if (!this.token) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { username, password } = this.credentials;
|
return `Bearer ${this.token}`;
|
||||||
const encoded = btoa(`${username}:${password}`);
|
|
||||||
return `Basic ${encoded}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validar credenciales (test básico)
|
// Hacer login (llamar al endpoint de login)
|
||||||
async validateCredentials(username, password) {
|
async login(username, password) {
|
||||||
try {
|
try {
|
||||||
// Intentar hacer una petición simple para validar las credenciales
|
const response = await fetch('/api/users/login', {
|
||||||
const encoded = btoa(`${username}:${password}`);
|
method: 'POST',
|
||||||
const response = await fetch('/api/stats', {
|
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',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Basic ${encoded}`,
|
'Authorization': `Bearer ${this.token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Si la petición funciona, las credenciales son válidas
|
if (response.ok) {
|
||||||
// Nota: stats no requiere auth, pero podemos usar cualquier endpoint
|
const data = await response.json();
|
||||||
return response.ok || response.status !== 401;
|
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) {
|
} catch (error) {
|
||||||
|
console.error('Error validando sesión:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,8 +351,7 @@ const passwordForm = ref({
|
|||||||
|
|
||||||
const isAuthenticated = computed(() => authService.hasCredentials());
|
const isAuthenticated = computed(() => authService.hasCredentials());
|
||||||
const currentUser = computed(() => {
|
const currentUser = computed(() => {
|
||||||
const creds = authService.getCredentials();
|
return authService.getUsername() || '';
|
||||||
return creds.username || '';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatDate(dateString) {
|
function formatDate(dateString) {
|
||||||
@@ -461,17 +460,16 @@ async function handleChangePassword() {
|
|||||||
newPassword: passwordForm.value.newPassword,
|
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
|
// Invalidar la sesión actual - el usuario deberá hacer login nuevamente
|
||||||
const creds = authService.getCredentials();
|
// El backend ya invalidó todas las sesiones, así que limpiamos localmente también
|
||||||
if (creds.username === currentUser.value) {
|
setTimeout(async () => {
|
||||||
authService.saveCredentials(currentUser.value, passwordForm.value.newPassword);
|
await authService.logout();
|
||||||
}
|
|
||||||
|
|
||||||
// Limpiar formulario después de 2 segundos
|
|
||||||
setTimeout(() => {
|
|
||||||
closeChangePasswordModal();
|
closeChangePasswordModal();
|
||||||
|
// Recargar página para forzar nuevo login
|
||||||
|
// El evento auth-required se disparará automáticamente cuando intente cargar datos
|
||||||
|
window.location.reload();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error cambiando contraseña:', error);
|
console.error('Error cambiando contraseña:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user