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 @@
+
-
-
-
+
+
+
@@ -122,196 +125,35 @@
-
+
-
+
+
+
+
-
-
-
-
-
![]()
$event.target.style.display = 'none'"
- />
-
- 📦
-
-
-
-
-
-
-
- {{ toast.platform?.toUpperCase() }}
-
-
-
- {{ toast.title || 'Nuevo artículo' }}
-
-
- {{ toast.price }} {{ toast.currency || '€' }}
-
-
- Ver artículo →
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

-
-
-
Iniciar Sesión
-
Wallabicher Admin Panel
-
-
-
-
-
-
-
-
-
- Ingresa tus credenciales para acceder al panel de administración.
-
-
-
-
-
-
+
@@ -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 @@
+
+
+
+
+
![]()
$event.target.style.display = 'none'"
+ />
+
+ 📦
+
+
+
+
+
+
+
+ {{ toast.platform?.toUpperCase() }}
+
+
+
+ {{ toast.title || 'Nuevo artículo' }}
+
+
+ {{ toast.price }} {{ toast.currency || '€' }}
+
+
+ Ver artículo →
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
Wallabicher
+
Admin Panel
+
+
+
+
+
+
+
+ Bienvenido de vuelta
+
+
+ Gestiona y monitoriza tus búsquedas de productos con nuestro panel de administración profesional.
+
+
+
+
+
+
+
+
+
Monitoreo en tiempo real
+
Recibe notificaciones instantáneas de nuevos artículos
+
+
+
+
+
+
Gestión avanzada
+
Control total sobre workers, usuarios y configuraciones
+
+
+
+
+
+
Seguridad empresarial
+
Autenticación robusta y gestión de permisos
+
+
+
+
+
+
+ © {{ new Date().getFullYear() }} Wallabicher. Todos los derechos reservados.
+
+
+
+
+
+
+
+
+
+
+

+
+
+
Wallabicher
+
Admin Panel
+
+
+
+
+
+
+
+ Iniciar Sesión
+
+
+ Ingresa tus credenciales para acceder al panel
+
+
+
+
+
+
+
+
+
+
+
+
+ ¿Necesitas ayuda? Contacta con el administrador del sistema
+
+
+
+
+
+
+
+
+
+
+
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);