diff --git a/web/backend/README.md b/web/backend/README.md index 2cb0a90..1b09279 100644 --- a/web/backend/README.md +++ b/web/backend/README.md @@ -50,7 +50,7 @@ backend/ ### Routes (`routes/`) Cada archivo maneja un conjunto relacionado de endpoints: -- **index.js**: `/api/stats`, `/api/cache` +- **index.js**: `/api/stats` - **workers.js**: `/api/workers` (GET, PUT) - **articles.js**: `/api/articles` (GET, search) - **favorites.js**: `/api/favorites` (GET, POST, DELETE) diff --git a/web/backend/routes/articles.js b/web/backend/routes/articles.js index 5862f92..91f6655 100644 --- a/web/backend/routes/articles.js +++ b/web/backend/routes/articles.js @@ -4,15 +4,6 @@ import { basicAuthMiddleware } from '../middlewares/auth.js'; const router = express.Router(); -router.delete('/', basicAuthMiddleware, async (req, res) => { - try { - const count = await clearAllArticles(); - res.json({ success: true, message: `Todos los artículos eliminados: ${count} artículos borrados`, count }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - // Obtener artículos notificados (requiere autenticación obligatoria) router.get('/', basicAuthMiddleware, async (req, res) => { try { diff --git a/web/backend/routes/index.js b/web/backend/routes/index.js index 909bbdb..29ada9a 100644 --- a/web/backend/routes/index.js +++ b/web/backend/routes/index.js @@ -1,5 +1,5 @@ import express from 'express'; -import { getFavorites, getNotifiedArticles, getDB, getWorkers, clearAllArticles } from '../services/mongodb.js'; +import { getFavorites, getNotifiedArticles, getDB, getWorkers } from '../services/mongodb.js'; import { basicAuthMiddleware } from '../middlewares/auth.js'; import { adminAuthMiddleware } from '../middlewares/adminAuth.js'; import { broadcast } from '../services/websocket.js'; @@ -92,75 +92,5 @@ router.get('/stats', basicAuthMiddleware, async (req, res) => { } }); -// Limpiar toda la caché de MongoDB (requiere autenticación de administrador) -router.delete('/cache', basicAuthMiddleware, adminAuthMiddleware, async (req, res) => { - try { - const db = getDB(); - if (!db) { - return res.status(500).json({ error: 'MongoDB no está disponible' }); - } - - // Eliminar todos los artículos - const count = await clearAllArticles(); - - // Notificar a los clientes WebSocket - broadcast({ - type: 'cache_cleared', - data: { count, timestamp: Date.now() } - }); - - // También notificar actualización de artículos (ahora está vacío) - broadcast({ type: 'articles_updated', data: [] }); - - // También actualizar favoritos (debería estar vacío ahora) - const favorites = await getFavorites(null); - broadcast({ type: 'favorites_updated', data: favorites, username: null }); - - res.json({ - success: true, - message: `Todos los artículos eliminados: ${count} artículos borrados`, - count - }); - } catch (error) { - console.error('Error limpiando cache de MongoDB:', error); - res.status(500).json({ error: error.message }); - } -}); - -// Endpoint específico para borrar artículos (alias de /cache para claridad) -router.delete('/articles', basicAuthMiddleware, adminAuthMiddleware, async (req, res) => { - try { - const db = getDB(); - if (!db) { - return res.status(500).json({ error: 'MongoDB no está disponible' }); - } - - // Eliminar todos los artículos - const count = await clearAllArticles(); - - // Notificar a los clientes WebSocket - broadcast({ - type: 'articles_cleared', - data: { count, timestamp: Date.now() } - }); - - // También notificar actualización de artículos (ahora está vacío) - broadcast({ type: 'articles_updated', data: [] }); - - // También actualizar favoritos (debería estar vacío ahora) - const favorites = await getFavorites(null); - broadcast({ type: 'favorites_updated', data: favorites, username: null }); - - res.json({ - success: true, - message: `Todos los artículos eliminados: ${count} artículos borrados`, - count - }); - } catch (error) { - console.error('Error borrando artículos:', error); - res.status(500).json({ error: error.message }); - } -}); - export default router; diff --git a/web/backend/services/mongodb.js b/web/backend/services/mongodb.js index 7f09ea2..2f2b3c8 100644 --- a/web/backend/services/mongodb.js +++ b/web/backend/services/mongodb.js @@ -799,21 +799,6 @@ export async function updateArticleFavorite(platform, id, is_favorite, username) } } -export async function clearAllArticles() { - if (!db) { - return 0; - } - - try { - const articlesCollection = db.collection('articles'); - const result = await articlesCollection.deleteMany({}); - return result.deletedCount; - } catch (error) { - console.error('Error limpiando artículos:', error.message); - return 0; - } -} - // Cerrar conexión export async function closeMongoDB() { if (mongoClient) { diff --git a/web/backend/services/websocket.js b/web/backend/services/websocket.js index 15fa871..d053521 100644 --- a/web/backend/services/websocket.js +++ b/web/backend/services/websocket.js @@ -1,16 +1,93 @@ import { WebSocketServer } from 'ws'; +import { getDB, getSession, getUser, deleteSession as deleteSessionFromDB } from './mongodb.js'; let wss = null; +// Duración de la sesión en milisegundos (24 horas) +const SESSION_DURATION = 24 * 60 * 60 * 1000; + // Inicializar WebSocket Server export function initWebSocket(server) { wss = new WebSocketServer({ server, path: '/ws' }); - wss.on('connection', (ws) => { - console.log('Cliente WebSocket conectado'); + wss.on('connection', async (ws, req) => { + // Extraer token de los query parameters + const url = new URL(req.url, `http://${req.headers.host}`); + const token = url.searchParams.get('token'); + + if (!token) { + console.log('Intento de conexión WebSocket sin token'); + ws.close(1008, 'Token requerido'); + return; + } + + // Validar token + try { + const db = getDB(); + + if (!db) { + console.error('MongoDB no disponible para validar WebSocket'); + ws.close(1011, 'Servicio no disponible'); + return; + } + + // Verificar token en MongoDB + const session = await getSession(token); + + if (!session) { + console.log('Intento de conexión WebSocket con token inválido'); + ws.close(1008, 'Token inválido'); + return; + } + + // Verificar que la sesión no haya expirado + if (session.expiresAt && new Date(session.expiresAt) < new Date()) { + await deleteSessionFromDB(token); + console.log('Intento de conexión WebSocket con sesión expirada'); + ws.close(1008, 'Sesión expirada'); + return; + } + + // Verificar que el usuario aún existe + const user = await getUser(session.username); + + if (!user) { + // Eliminar sesión si el usuario ya no existe + await deleteSessionFromDB(token); + console.log('Intento de conexión WebSocket con usuario inexistente'); + ws.close(1008, 'Usuario no encontrado'); + return; + } + + // Actualizar expiración de la sesión (refresh) + const sessionsCollection = db.collection('sessions'); + const newExpiresAt = new Date(Date.now() + SESSION_DURATION); + await sessionsCollection.updateOne( + { token }, + { $set: { expiresAt: newExpiresAt } } + ); + + // Autenticación exitosa - almacenar información del usuario en el websocket + ws.user = { + username: session.username, + role: user.role || 'user', + token: token + }; + + console.log(`Cliente WebSocket conectado: ${session.username} (${user.role || 'user'})`); + + } catch (error) { + console.error('Error validando token WebSocket:', error); + ws.close(1011, 'Error de autenticación'); + return; + } ws.on('close', () => { - console.log('Cliente WebSocket desconectado'); + if (ws.user) { + console.log(`Cliente WebSocket desconectado: ${ws.user.username}`); + } else { + console.log('Cliente WebSocket desconectado'); + } }); ws.on('error', (error) => { diff --git a/web/frontend/CONFIGURACION_BACKEND.md b/web/frontend/CONFIGURACION_BACKEND.md new file mode 100644 index 0000000..97f8c73 --- /dev/null +++ b/web/frontend/CONFIGURACION_BACKEND.md @@ -0,0 +1,44 @@ +# Configuración del Backend + +Este documento explica cómo configurar el frontend para conectarse a diferentes backends. + +## Configuración mediante Variables de Entorno + +El frontend usa la variable de entorno `VITE_API_BASE_URL` para determinar la URL del backend. + +### Para desarrollo local (por defecto) + +Si no defines `VITE_API_BASE_URL`, el frontend usará `/api` que será manejado por el proxy de Vite configurado en `vite.config.js` (redirige a `http://localhost:3001`). + +### Para backend en producción + +Crea un archivo `.env.local` en la raíz del directorio `frontend` con el siguiente contenido: + +```bash +VITE_API_BASE_URL=https://wb.pribyte.cloud/api +``` + +**Nota:** Los archivos `.env.local` están en `.gitignore` y no se subirán al repositorio. + +## Cómo usar + +1. **Desarrollo local:** + - No necesitas hacer nada, funciona por defecto con el proxy de Vite + +2. **Backend en producción:** + - Crea el archivo `.env.local` con la URL del backend + - Reinicia el servidor de desarrollo (`npm run dev`) para que cargue las nuevas variables + +## Ejemplo de archivo `.env.local` + +```bash +# Backend en producción +VITE_API_BASE_URL=https://wb.pribyte.cloud/api +``` + +## Notas importantes + +- Las variables de entorno que empiezan con `VITE_` son expuestas al código del cliente +- Después de modificar `.env.local`, necesitas reiniciar el servidor de desarrollo +- El WebSocket también se configurará automáticamente basándose en la URL del backend + diff --git a/web/frontend/src/App.vue b/web/frontend/src/App.vue index 604b8d0..fc95a47 100644 --- a/web/frontend/src/App.vue +++ b/web/frontend/src/App.vue @@ -1,5 +1,7 @@ @@ -331,12 +173,12 @@ import { BellIcon, BellSlashIcon, ArrowRightOnRectangleIcon, - ArrowLeftOnRectangleIcon, } from '@heroicons/vue/24/outline'; import pushNotificationService from './services/pushNotifications'; import authService from './services/auth'; import { useRouter } from 'vue-router'; import api from './services/api'; +import ToastContainer from './components/ToastContainer.vue'; const allNavItems = [ { path: '/', name: 'Dashboard', icon: HomeIcon, adminOnly: false }, @@ -351,20 +193,10 @@ const router = useRouter(); const wsConnected = ref(false); const sidebarCollapsed = ref(false); const darkMode = ref(false); -const toasts = ref([]); const pushEnabled = ref(false); -const showLoginModal = ref(false); -const globalLoginError = ref(''); -const globalLoginLoading = ref(false); const currentUser = ref(authService.getUsername() || null); const isAdmin = ref(false); -const globalLoginForm = ref({ - username: '', - password: '', - remember: true, -}); let ws = null; -let toastIdCounter = 0; const isDark = computed(() => darkMode.value); const isAuthenticated = computed(() => authService.hasCredentials()); @@ -380,30 +212,6 @@ const navItems = computed(() => { }); }); -function checkUserRole() { - currentUser.value = authService.getUsername() || null; - isAdmin.value = currentUser.value === 'admin'; // Simplificación temporal -} - -function addToast(article) { - const id = ++toastIdCounter; - toasts.value.push({ - id, - ...article, - }); - - // Auto-remover después de 5 segundos (más corto para toasts discretos) - setTimeout(() => { - removeToast(id); - }, 5000); -} - -function removeToast(id) { - const index = toasts.value.findIndex(t => t.id === id); - if (index > -1) { - toasts.value.splice(index, 1); - } -} function toggleDarkMode() { darkMode.value = !darkMode.value; @@ -469,69 +277,33 @@ async function checkPushStatus() { pushEnabled.value = hasSubscription; } -async function handleGlobalLogin() { - globalLoginError.value = ''; - globalLoginLoading.value = true; - - if (!globalLoginForm.value.username || !globalLoginForm.value.password) { - globalLoginError.value = 'Usuario y contraseña son requeridos'; - globalLoginLoading.value = false; - return; - } - - try { - // Usar el nuevo método login que genera un token - await authService.login( - globalLoginForm.value.username, - globalLoginForm.value.password - ); - - // 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); - globalLoginError.value = error.message || 'Usuario o contraseña incorrectos'; - authService.clearSession(); - } finally { - globalLoginLoading.value = false; - } -} - -function closeLoginModal() { - showLoginModal.value = false; - globalLoginError.value = ''; - globalLoginForm.value = { - username: '', - password: '', - remember: true, - }; -} - -function handleAuthRequired(event) { - showLoginModal.value = true; - if (event.detail?.message) { - globalLoginError.value = event.detail.message; - } -} - function getCurrentPageTitle() { const currentItem = navItems.value.find(item => item.path === router.currentRoute.value.path); return currentItem ? currentItem.name : 'Dashboard'; } function handleAuthChange() { - checkUserRole(); + currentUser.value = authService.getUsername() || null; + isAdmin.value = authService.isAdmin(); + // Reconectar websocket cuando cambie la autenticación (login) + if (authService.hasCredentials()) { + connectWebSocket(); + } } async function handleLogout() { + // Cerrar conexión WebSocket antes de hacer logout + if (ws) { + ws.close(); + ws = null; + wsConnected.value = false; + } + // Llamar al endpoint de logout e invalidar token await authService.logout(); - // Redirigir al dashboard después del logout - router.push('/'); + // Redirigir a login después del logout + router.push('/login'); // Disparar evento para que los componentes se actualicen window.dispatchEvent(new CustomEvent('auth-logout')); @@ -542,45 +314,77 @@ async function handleLogout() { onMounted(async () => { initDarkMode(); - checkUserRole(); - connectWebSocket(); + currentUser.value = authService.getUsername() || null; + isAdmin.value = authService.isAdmin(); await checkPushStatus(); - // Cargar username guardado si existe (pero no la contraseña) - if (authService.hasCredentials()) { - 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(); - checkUserRole(); - } - } - - // Escuchar eventos de autenticación requerida - window.addEventListener('auth-required', handleAuthRequired); + // Escuchar eventos de autenticación window.addEventListener('auth-login', handleAuthChange); window.addEventListener('auth-logout', handleAuthChange); + + // Si hay credenciales, validar y conectar websocket + if (authService.hasCredentials()) { + // Validar si el token sigue siendo válido + const isValid = await authService.validateSession(); + if (!isValid) { + // Si el token expiró, limpiar sesión y redirigir a login + authService.clearSession(); + currentUser.value = authService.getUsername() || null; + isAdmin.value = authService.isAdmin(); + if (router.currentRoute.value.path !== '/login') { + router.push('/login'); + } + } else { + // Solo conectar websocket si el token es válido + connectWebSocket(); + } + } }); onUnmounted(() => { - window.removeEventListener('auth-required', handleAuthRequired); window.removeEventListener('auth-login', handleAuthChange); window.removeEventListener('auth-logout', handleAuthChange); + if (ws) { ws.close(); } }); function connectWebSocket() { - // Use relative path so Vite proxy can handle it - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws`; + // Cerrar conexión existente si hay una + if (ws) { + ws.close(); + ws = null; + } + + // Verificar si hay token de autenticación + const token = authService.getToken(); + if (!token) { + console.log('No hay token de autenticación, no se conectará WebSocket'); + wsConnected.value = false; + return; + } + + let wsUrl; + + // Si hay una URL de API configurada, usarla para WebSocket también + const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; + if (apiBaseUrl && apiBaseUrl !== '/api') { + // Extraer el host de la URL de la API y construir la URL del WebSocket + try { + const url = new URL(apiBaseUrl); + const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; + wsUrl = `${protocol}//${url.host}/ws?token=${encodeURIComponent(token)}`; + } catch (e) { + // Si falla el parsing, usar la configuración por defecto + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + wsUrl = `${protocol}//${window.location.host}/ws?token=${encodeURIComponent(token)}`; + } + } else { + // Use relative path so Vite proxy can handle it + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + wsUrl = `${protocol}//${window.location.host}/ws?token=${encodeURIComponent(token)}`; + } ws = new WebSocket(wsUrl); @@ -589,10 +393,24 @@ function connectWebSocket() { console.log('WebSocket conectado'); }; - ws.onclose = () => { + ws.onclose = (event) => { wsConnected.value = false; - console.log('WebSocket desconectado, reintentando...'); - setTimeout(connectWebSocket, 3000); + + // Si el cierre fue por autenticación fallida (código 1008), no reintentar + if (event.code === 1008) { + console.log('WebSocket cerrado: autenticación fallida'); + // Si el token aún existe, intentar reconectar después de un delay más largo + // para dar tiempo a que el usuario se autentique de nuevo + if (authService.hasCredentials()) { + setTimeout(connectWebSocket, 5000); + } + } else { + // Para otros errores, reintentar después de 3 segundos si hay token + if (authService.hasCredentials()) { + console.log('WebSocket desconectado, reintentando...'); + setTimeout(connectWebSocket, 3000); + } + } }; ws.onerror = (error) => { @@ -603,15 +421,7 @@ function connectWebSocket() { ws.onmessage = (event) => { const data = JSON.parse(event.data); - // Manejar notificaciones de artículos nuevos - if (data.type === 'new_articles' && data.data) { - // Mostrar toasts para cada artículo nuevo - for (const article of data.data) { - addToast(article); - } - } - - // Los componentes individuales manejarán otros mensajes + // Los componentes individuales manejarán los mensajes (incluyendo ToastContainer) window.dispatchEvent(new CustomEvent('ws-message', { detail: data })); }; } diff --git a/web/frontend/src/components/ToastContainer.vue b/web/frontend/src/components/ToastContainer.vue new file mode 100644 index 0000000..d4c77b7 --- /dev/null +++ b/web/frontend/src/components/ToastContainer.vue @@ -0,0 +1,127 @@ + + + + + + diff --git a/web/frontend/src/components/ToastNotification.vue b/web/frontend/src/components/ToastNotification.vue new file mode 100644 index 0000000..2796a23 --- /dev/null +++ b/web/frontend/src/components/ToastNotification.vue @@ -0,0 +1,68 @@ + + + + diff --git a/web/frontend/src/main.js b/web/frontend/src/main.js index 146bb9a..f457b8d 100644 --- a/web/frontend/src/main.js +++ b/web/frontend/src/main.js @@ -7,15 +7,18 @@ import Favorites from './views/Favorites.vue'; import Workers from './views/Workers.vue'; import Users from './views/Users.vue'; import Logs from './views/Logs.vue'; +import Login from './views/Login.vue'; import './style.css'; +import authService from './services/auth'; const routes = [ - { path: '/', component: Dashboard }, - { path: '/articles', component: Articles }, - { path: '/favorites', component: Favorites }, - { path: '/workers', component: Workers }, - { path: '/users', component: Users }, - { path: '/logs', component: Logs }, + { path: '/login', component: Login, name: 'login' }, + { path: '/', component: Dashboard, meta: { requiresAuth: true } }, + { path: '/articles', component: Articles, meta: { requiresAuth: true } }, + { path: '/favorites', component: Favorites, meta: { requiresAuth: true } }, + { path: '/workers', component: Workers, meta: { requiresAuth: true } }, + { path: '/users', component: Users, meta: { requiresAuth: true } }, + { path: '/logs', component: Logs, meta: { requiresAuth: true } }, ]; const router = createRouter({ @@ -23,6 +26,43 @@ const router = createRouter({ routes, }); +// Guard de navegación para verificar autenticación +router.beforeEach(async (to, from, next) => { + // Si la ruta es /login y ya está autenticado, redirigir al dashboard + if (to.path === '/login') { + if (authService.hasCredentials()) { + const isValid = await authService.validateSession(); + if (isValid) { + next('/'); + return; + } + } + next(); + return; + } + + // Para todas las demás rutas, verificar autenticación + if (to.meta.requiresAuth) { + // Verificar si hay token almacenado + if (!authService.hasCredentials()) { + // No hay token, redirigir a login + next('/login'); + return; + } + + // Hay token, validar si sigue siendo válido + const isValid = await authService.validateSession(); + if (!isValid) { + // Token inválido o expirado, redirigir a login + next('/login'); + return; + } + } + + // Continuar la navegación + next(); +}); + const app = createApp(App); app.use(router); app.mount('#app'); diff --git a/web/frontend/src/services/api.js b/web/frontend/src/services/api.js index d9d4840..1b12e6e 100644 --- a/web/frontend/src/services/api.js +++ b/web/frontend/src/services/api.js @@ -1,8 +1,13 @@ import axios from 'axios'; import authService from './auth'; +// Usar variable de entorno si está disponible, sino usar '/api' (proxy en desarrollo) +const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'; + +console.log('baseURL', baseURL); + const api = axios.create({ - baseURL: '/api', + baseURL, headers: { 'Content-Type': 'application/json', }, @@ -119,18 +124,6 @@ export default { return response.data; }, - // Cache - async clearCache() { - const response = await api.delete('/cache'); - return response.data; - }, - - // Artículos - Borrar todos (solo admin) - async clearAllArticles() { - const response = await api.delete('/articles'); - return response.data; - }, - // Usuarios async getUsers() { const response = await api.get('/users'); diff --git a/web/frontend/src/style.css b/web/frontend/src/style.css index 9c0b061..229945e 100644 --- a/web/frontend/src/style.css +++ b/web/frontend/src/style.css @@ -185,6 +185,17 @@ } } +@keyframes slide-out { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } +} + @keyframes fade-in { from { opacity: 0; @@ -202,3 +213,16 @@ animation: fade-in 0.2s ease-out; } +/* Toast transitions */ +.toast-enter-active { + animation: slide-in 0.3s ease-out; +} + +.toast-leave-active { + animation: slide-out 0.3s ease-in; +} + +.toast-move { + transition: transform 0.3s ease; +} + diff --git a/web/frontend/src/views/Articles.vue b/web/frontend/src/views/Articles.vue index 77eb662..378657a 100644 --- a/web/frontend/src/views/Articles.vue +++ b/web/frontend/src/views/Articles.vue @@ -47,14 +47,6 @@ - @@ -192,14 +184,6 @@ const filteredArticles = computed(() => { }); -function checkUserRole() { - currentUser.value = authService.getUsername() || null; - isAdmin.value = currentUser.value === 'admin'; // Simplificación temporal - // Si no es admin, no permitir filtrar por username - if (!isAdmin.value && selectedUsername.value) { - selectedUsername.value = ''; - } -} async function loadArticles(reset = true, silent = false) { if (reset) { @@ -248,7 +232,12 @@ async function loadArticles(reset = true, silent = false) { } function handleAuthChange() { - checkUserRole(); + currentUser.value = authService.getUsername() || null; + isAdmin.value = authService.isAdmin(); + // Si no es admin, no permitir filtrar por username + if (!isAdmin.value && selectedUsername.value) { + selectedUsername.value = ''; + } if (currentUser.value) { loadArticles(); } @@ -260,69 +249,11 @@ function loadMore() { function handleWSMessage(event) { const data = event.detail; - if (data.type === 'articles_updated' || data.type === 'articles_cleared' || data.type === 'cache_cleared') { + if (data.type === 'articles_updated') { loadArticles(); } } -async function handleClearAllArticles() { - if (!isAdmin.value) { - alert('Solo los administradores pueden borrar todos los artículos'); - return; - } - - const confirmed = confirm( - '⚠️ ¿Estás seguro de que quieres borrar TODOS los artículos?\n\n' + - 'Esta acción eliminará permanentemente todos los artículos de la base de datos.\n' + - 'Esta acción NO se puede deshacer.\n\n' + - '¿Continuar?' - ); - - if (!confirmed) { - return; - } - - // Confirmación adicional - const doubleConfirmed = confirm( - '⚠️ ÚLTIMA CONFIRMACIÓN ⚠️\n\n' + - 'Estás a punto de borrar TODOS los artículos de la base de datos.\n' + - 'Esta acción es IRREVERSIBLE.\n\n' + - '¿Estás absolutamente seguro?' - ); - - if (!doubleConfirmed) { - return; - } - - loading.value = true; - - try { - const result = await api.clearAllArticles(); - alert(`✅ ${result.message || `Se borraron ${result.count || 0} artículos`}`); - - // Limpiar la vista - allArticles.value = []; - searchResults.value = []; - total.value = 0; - offset.value = 0; - - // Recargar artículos (ahora estará vacío) - await loadArticles(); - } catch (error) { - console.error('Error borrando artículos:', error); - - if (error.response?.status === 403) { - alert('❌ Error: No tienes permisos de administrador para realizar esta acción'); - } else if (error.response?.status === 401) { - alert('❌ Error: Debes estar autenticado para realizar esta acción'); - } else { - alert('❌ Error al borrar artículos: ' + (error.response?.data?.error || error.message || 'Error desconocido')); - } - } finally { - loading.value = false; - } -} - async function searchArticles(query) { if (!query.trim()) { searchResults.value = []; @@ -378,7 +309,12 @@ watch(searchQuery, (newQuery) => { onMounted(() => { - checkUserRole(); + currentUser.value = authService.getUsername() || null; + isAdmin.value = authService.isAdmin(); + // Si no es admin, no permitir filtrar por username + if (!isAdmin.value && selectedUsername.value) { + selectedUsername.value = ''; + } loadArticles(); window.addEventListener('ws-message', handleWSMessage); window.addEventListener('auth-logout', handleAuthChange); diff --git a/web/frontend/src/views/Dashboard.vue b/web/frontend/src/views/Dashboard.vue index 7079658..884002c 100644 --- a/web/frontend/src/views/Dashboard.vue +++ b/web/frontend/src/views/Dashboard.vue @@ -263,16 +263,10 @@ async function loadStats() { } } -function checkUserRole() { - currentUser.value = authService.getUsername() || null; - // Por ahora, asumimos que si no hay usuario o el usuario no es admin, no es admin - // En el futuro, se podría añadir un endpoint para verificar el rol - // Por defecto, asumimos que el usuario normal no es admin - isAdmin.value = currentUser.value === 'admin'; // Simplificación temporal -} function handleAuthChange() { - checkUserRole(); + currentUser.value = authService.getUsername() || null; + isAdmin.value = authService.isAdmin(); if (currentUser.value) { loadStats(); } @@ -288,7 +282,8 @@ function handleWSMessage(event) { let interval = null; onMounted(() => { - checkUserRole(); + currentUser.value = authService.getUsername() || null; + isAdmin.value = authService.isAdmin(); loadStats(); window.addEventListener('ws-message', handleWSMessage); window.addEventListener('auth-logout', handleAuthChange); diff --git a/web/frontend/src/views/Favorites.vue b/web/frontend/src/views/Favorites.vue index 3945142..05a487e 100644 --- a/web/frontend/src/views/Favorites.vue +++ b/web/frontend/src/views/Favorites.vue @@ -96,10 +96,6 @@ async function removeFavorite(platform, id) { } } -function checkUserRole() { - currentUser.value = authService.getUsername() || null; - isAdmin.value = currentUser.value === 'admin'; // Simplificación temporal -} function handleWSMessage(event) { const data = event.detail; @@ -112,14 +108,16 @@ function handleWSMessage(event) { } function handleAuthChange() { - checkUserRole(); + currentUser.value = authService.getUsername() || null; + isAdmin.value = authService.isAdmin(); if (currentUser.value) { loadFavorites(); } } onMounted(() => { - checkUserRole(); + currentUser.value = authService.getUsername() || null; + isAdmin.value = authService.isAdmin(); loadFavorites(); window.addEventListener('ws-message', handleWSMessage); window.addEventListener('auth-logout', handleAuthChange); diff --git a/web/frontend/src/views/Login.vue b/web/frontend/src/views/Login.vue new file mode 100644 index 0000000..8bfd3c2 --- /dev/null +++ b/web/frontend/src/views/Login.vue @@ -0,0 +1,300 @@ + + + + + diff --git a/web/frontend/src/views/Logs.vue b/web/frontend/src/views/Logs.vue index d2188e4..d424f8f 100644 --- a/web/frontend/src/views/Logs.vue +++ b/web/frontend/src/views/Logs.vue @@ -126,10 +126,6 @@ function getLogColor(log) { return 'text-green-400'; } -function checkUserRole() { - currentUser.value = authService.getUsername() || null; - isAdmin.value = currentUser.value === 'admin'; // Simplificación temporal -} async function loadLogs(forceReload = false, shouldScroll = null) { // Verificar que el usuario es admin antes de cargar logs @@ -245,7 +241,8 @@ function handleWSMessage(event) { } onMounted(() => { - checkUserRole(); + currentUser.value = authService.getUsername() || null; + isAdmin.value = authService.isAdmin(); loadLogs(true, true); // Primera carga forzada siempre hace scroll window.addEventListener('ws-message', handleWSMessage);