Refactor routing in App.vue and Login.vue to redirect to the home page instead of the dashboard. Remove unused Users.vue component to streamline the codebase. Update Dockerfile to adjust Nginx configuration for landing page deployment.
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
>
|
>
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="flex items-center justify-between h-16 px-4 border-b border-gray-200 dark:border-gray-700">
|
<div class="flex items-center justify-between h-16 px-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
<router-link v-if="!sidebarCollapsed" to="/dashboard" class="flex items-center space-x-3 flex-1 min-w-0 group">
|
<router-link v-if="!sidebarCollapsed" to="/" class="flex items-center space-x-3 flex-1 min-w-0 group">
|
||||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg overflow-hidden ring-2 ring-gray-200 dark:ring-gray-700 group-hover:ring-primary-500 transition-all">
|
<div class="flex-shrink-0 w-10 h-10 rounded-lg overflow-hidden ring-2 ring-gray-200 dark:ring-gray-700 group-hover:ring-primary-500 transition-all">
|
||||||
<img
|
<img
|
||||||
src="/logo.jpg"
|
src="/logo.jpg"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<p class="text-xs text-gray-500 dark:text-gray-400">Admin Panel</p>
|
<p class="text-xs text-gray-500 dark:text-gray-400">Admin Panel</p>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link v-else to="/dashboard" class="flex items-center justify-center w-full group">
|
<router-link v-else to="/" class="flex items-center justify-center w-full group">
|
||||||
<div class="w-10 h-10 rounded-lg overflow-hidden ring-2 ring-gray-200 dark:ring-gray-700 group-hover:ring-primary-500 transition-all">
|
<div class="w-10 h-10 rounded-lg overflow-hidden ring-2 ring-gray-200 dark:ring-gray-700 group-hover:ring-primary-500 transition-all">
|
||||||
<img
|
<img
|
||||||
src="/logo.jpg"
|
src="/logo.jpg"
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ async function handleLogin() {
|
|||||||
|
|
||||||
// Si llegamos aquí, el login fue exitoso
|
// Si llegamos aquí, el login fue exitoso
|
||||||
// Redirigir al dashboard
|
// Redirigir al dashboard
|
||||||
router.push('/dashboard');
|
router.push('/');
|
||||||
|
|
||||||
// Disparar evento para que App.vue se actualice
|
// Disparar evento para que App.vue se actualice
|
||||||
window.dispatchEvent(new CustomEvent('auth-login'));
|
window.dispatchEvent(new CustomEvent('auth-login'));
|
||||||
@@ -275,7 +275,7 @@ async function handleLogin() {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Si ya está autenticado, redirigir al dashboard
|
// Si ya está autenticado, redirigir al dashboard
|
||||||
if (authService.hasCredentials()) {
|
if (authService.hasCredentials()) {
|
||||||
router.push('/dashboard');
|
router.push('/');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,706 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 sm:gap-4 mb-4 sm:mb-6">
|
|
||||||
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100">Gestión de Usuarios</h1>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<button
|
|
||||||
v-if="isAdmin"
|
|
||||||
@click="showAddModal = true"
|
|
||||||
class="btn btn-primary text-xs sm:text-sm whitespace-nowrap"
|
|
||||||
>
|
|
||||||
+ Crear Usuario
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!isAdmin" class="card text-center py-12">
|
|
||||||
<p class="text-gray-600 dark:text-gray-400 text-lg font-semibold mb-2">Acceso Denegado</p>
|
|
||||||
<p class="text-gray-500 dark:text-gray-500 text-sm">
|
|
||||||
Solo los administradores pueden acceder a esta sección.
|
|
||||||
</p>
|
|
||||||
<router-link to="/settings" class="btn btn-primary mt-4 inline-block">
|
|
||||||
Ir a Configuración
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="loading" class="text-center py-12">
|
|
||||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
|
||||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Cargando usuarios...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="space-y-4">
|
|
||||||
<!-- Lista de usuarios -->
|
|
||||||
<div v-if="users.length > 0" class="grid grid-cols-1 gap-4">
|
|
||||||
<div
|
|
||||||
v-for="user in users"
|
|
||||||
:key="user.username"
|
|
||||||
class="card hover:shadow-lg transition-shadow"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ user.username }}</h3>
|
|
||||||
<span
|
|
||||||
v-if="user.username === currentUser"
|
|
||||||
class="px-2 py-1 text-xs font-semibold rounded bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
|
|
||||||
>
|
|
||||||
Tú
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="user.role === 'admin'"
|
|
||||||
class="px-2 py-1 text-xs font-semibold rounded bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200"
|
|
||||||
>
|
|
||||||
Admin
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm text-gray-600 dark:text-gray-400 mb-3">
|
|
||||||
<div v-if="user.createdAt">
|
|
||||||
<span class="font-medium">Creado:</span>
|
|
||||||
<span class="ml-2">{{ formatDate(user.createdAt) }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="user.createdBy">
|
|
||||||
<span class="font-medium">Por:</span>
|
|
||||||
<span class="ml-2">{{ user.createdBy }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="user.updatedAt">
|
|
||||||
<span class="font-medium">Actualizado:</span>
|
|
||||||
<span class="ml-2">{{ formatDate(user.updatedAt) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Información de suscripción -->
|
|
||||||
<div v-if="userSubscriptions[user.username]" class="mt-3 p-3 bg-gradient-to-r from-primary-50 to-teal-50 dark:from-primary-900/20 dark:to-teal-900/20 rounded-lg border border-primary-200 dark:border-primary-800">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<span class="text-xs font-semibold text-gray-700 dark:text-gray-300">Plan:</span>
|
|
||||||
<span class="ml-2 text-sm font-bold text-primary-700 dark:text-primary-400">
|
|
||||||
{{ userSubscriptions[user.username].plan?.name || 'Gratis' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
:class="userSubscriptions[user.username].subscription?.status === 'active' ? 'badge badge-success' : 'badge badge-warning'"
|
|
||||||
class="text-xs"
|
|
||||||
>
|
|
||||||
{{ userSubscriptions[user.username].subscription?.status === 'active' ? 'Activo' : 'Inactivo' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="userSubscriptions[user.username].usage" class="mt-2 text-xs text-gray-600 dark:text-gray-400">
|
|
||||||
<span>Uso: {{ userSubscriptions[user.username].usage.workers }} / {{ userSubscriptions[user.username].usage.maxWorkers === 'Ilimitado' ? '∞' : userSubscriptions[user.username].usage.maxWorkers }} búsquedas</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2 flex-wrap">
|
|
||||||
<button
|
|
||||||
v-if="isAdmin"
|
|
||||||
@click="openSubscriptionModal(user.username)"
|
|
||||||
class="btn btn-primary text-xs sm:text-sm"
|
|
||||||
title="Gestionar suscripción"
|
|
||||||
>
|
|
||||||
💳 Plan
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="user.username !== currentUser && isAdmin"
|
|
||||||
@click="confirmDeleteUser(user.username)"
|
|
||||||
class="btn btn-danger text-xs sm:text-sm"
|
|
||||||
title="Eliminar usuario"
|
|
||||||
>
|
|
||||||
🗑️ Eliminar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="card text-center py-12">
|
|
||||||
<p class="text-gray-600 dark:text-gray-400 mb-4">No hay usuarios configurados</p>
|
|
||||||
<button @click="showAddModal = true" class="btn btn-primary">
|
|
||||||
+ Crear primer usuario
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal para crear/editar usuario -->
|
|
||||||
<div
|
|
||||||
v-if="showAddModal"
|
|
||||||
class="fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 flex items-center justify-center z-50 p-4"
|
|
||||||
@click.self="closeAddModal"
|
|
||||||
>
|
|
||||||
<div class="card max-w-md w-full max-h-[90vh] overflow-y-auto">
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100">Crear Usuario</h2>
|
|
||||||
<button
|
|
||||||
@click="closeAddModal"
|
|
||||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
||||||
title="Cerrar"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form @submit.prevent="handleCreateUser" class="space-y-4">
|
|
||||||
<div v-if="addError" class="bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded">
|
|
||||||
{{ addError }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Nombre de usuario <span class="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="userForm.username"
|
|
||||||
type="text"
|
|
||||||
class="input"
|
|
||||||
placeholder="nuevo_usuario"
|
|
||||||
required
|
|
||||||
minlength="3"
|
|
||||||
autocomplete="username"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
Mínimo 3 caracteres
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Contraseña <span class="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="userForm.password"
|
|
||||||
type="password"
|
|
||||||
class="input"
|
|
||||||
placeholder="••••••••"
|
|
||||||
required
|
|
||||||
minlength="6"
|
|
||||||
autocomplete="new-password"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
Mínimo 6 caracteres
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Confirmar contraseña <span class="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="userForm.passwordConfirm"
|
|
||||||
type="password"
|
|
||||||
class="input"
|
|
||||||
placeholder="••••••••"
|
|
||||||
required
|
|
||||||
minlength="6"
|
|
||||||
autocomplete="new-password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col-reverse sm:flex-row justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="closeAddModal"
|
|
||||||
class="btn btn-secondary text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary text-sm sm:text-base"
|
|
||||||
:disabled="loadingAction"
|
|
||||||
>
|
|
||||||
{{ loadingAction ? 'Creando...' : 'Crear Usuario' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal para gestionar suscripción (solo admin) -->
|
|
||||||
<div
|
|
||||||
v-if="showSubscriptionModal && selectedUserForSubscription"
|
|
||||||
class="fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 flex items-center justify-center z-50 p-4"
|
|
||||||
@click.self="closeSubscriptionModal"
|
|
||||||
>
|
|
||||||
<div class="card max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100">
|
|
||||||
Gestionar Suscripción: {{ selectedUserForSubscription }}
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
@click="closeSubscriptionModal"
|
|
||||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
||||||
title="Cerrar"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="loadingSubscription" class="text-center py-8">
|
|
||||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
|
||||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Cargando información...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="space-y-6">
|
|
||||||
<!-- Información actual -->
|
|
||||||
<div v-if="selectedUserSubscription && selectedUserSubscription.subscription" class="p-4 bg-gradient-to-br from-primary-50 to-teal-50 dark:from-primary-900/20 dark:to-teal-900/20 rounded-xl border border-primary-200 dark:border-primary-800">
|
|
||||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Plan Actual</h3>
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<p class="text-xs text-gray-600 dark:text-gray-400">Plan</p>
|
|
||||||
<p class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
|
||||||
{{ selectedUserSubscription.subscription?.plan?.name || 'Gratis' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-xs text-gray-600 dark:text-gray-400">Estado</p>
|
|
||||||
<span
|
|
||||||
:class="selectedUserSubscription.subscription?.status === 'active' ? 'badge badge-success' : 'badge badge-warning'"
|
|
||||||
>
|
|
||||||
{{ selectedUserSubscription.subscription?.status === 'active' ? 'Activo' : 'Inactivo' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedUserSubscription.usage">
|
|
||||||
<p class="text-xs text-gray-600 dark:text-gray-400">Búsquedas</p>
|
|
||||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{{ selectedUserSubscription.usage.workers }} / {{ selectedUserSubscription.usage.maxWorkers === 'Ilimitado' ? '∞' : selectedUserSubscription.usage.maxWorkers }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cambiar plan -->
|
|
||||||
<div>
|
|
||||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Cambiar Plan</h3>
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
||||||
<button
|
|
||||||
v-for="plan in availablePlans"
|
|
||||||
:key="plan.id"
|
|
||||||
@click="subscriptionForm.planId = plan.id"
|
|
||||||
class="p-4 rounded-lg border-2 text-left transition-all"
|
|
||||||
:class="
|
|
||||||
subscriptionForm.planId === plan.id
|
|
||||||
? 'border-primary-500 dark:border-primary-400 bg-primary-50 dark:bg-primary-900/20'
|
|
||||||
: 'border-gray-200 dark:border-gray-700 hover:border-primary-300 dark:hover:border-primary-700'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="font-bold text-gray-900 dark:text-gray-100">{{ plan.name }}</p>
|
|
||||||
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">{{ plan.description }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="subscriptionForm.planId === plan.id" class="text-primary-600 dark:text-primary-400">
|
|
||||||
✓
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Formulario de actualización -->
|
|
||||||
<form @submit.prevent="handleUpdateUserSubscription" class="space-y-4">
|
|
||||||
<div v-if="subscriptionError" class="bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded">
|
|
||||||
{{ subscriptionError }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="subscriptionSuccess" class="bg-green-100 dark:bg-green-900/30 border border-green-400 dark:border-green-700 text-green-700 dark:text-green-300 px-4 py-3 rounded">
|
|
||||||
{{ subscriptionSuccess }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Estado de la suscripción
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
v-model="subscriptionForm.status"
|
|
||||||
class="input"
|
|
||||||
>
|
|
||||||
<option value="active">Activo</option>
|
|
||||||
<option value="inactive">Inactivo</option>
|
|
||||||
<option value="cancelled">Cancelado</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
v-model="subscriptionForm.cancelAtPeriodEnd"
|
|
||||||
type="checkbox"
|
|
||||||
id="cancelAtPeriodEnd"
|
|
||||||
class="w-4 h-4 text-primary-600 rounded focus:ring-primary-500"
|
|
||||||
/>
|
|
||||||
<label for="cancelAtPeriodEnd" class="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
Cancelar al final del período
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col-reverse sm:flex-row justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="closeSubscriptionModal"
|
|
||||||
class="btn btn-secondary text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary text-sm sm:text-base"
|
|
||||||
:disabled="loadingAction"
|
|
||||||
>
|
|
||||||
{{ loadingAction ? 'Actualizando...' : 'Actualizar Suscripción' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal de confirmación para eliminar -->
|
|
||||||
<div
|
|
||||||
v-if="userToDelete"
|
|
||||||
class="fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 flex items-center justify-center z-50 p-4"
|
|
||||||
@click.self="userToDelete = null"
|
|
||||||
>
|
|
||||||
<div class="card max-w-md w-full">
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100">Confirmar Eliminación</h2>
|
|
||||||
<button
|
|
||||||
@click="userToDelete = null"
|
|
||||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
||||||
title="Cerrar"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
|
||||||
¿Estás seguro de que deseas eliminar al usuario <strong>{{ userToDelete }}</strong>?
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-red-600 dark:text-red-400 mb-4">
|
|
||||||
Esta acción no se puede deshacer.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="flex flex-col-reverse sm:flex-row justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="userToDelete = null"
|
|
||||||
class="btn btn-secondary text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="handleDeleteUser"
|
|
||||||
class="btn btn-danger text-sm sm:text-base"
|
|
||||||
:disabled="loadingAction"
|
|
||||||
>
|
|
||||||
{{ loadingAction ? 'Eliminando...' : 'Eliminar Usuario' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
|
||||||
import api from '../services/api';
|
|
||||||
import authService from '../services/auth';
|
|
||||||
|
|
||||||
const users = ref([]);
|
|
||||||
const loading = ref(true);
|
|
||||||
const loadingAction = ref(false);
|
|
||||||
const showAddModal = ref(false);
|
|
||||||
const showSubscriptionModal = ref(false);
|
|
||||||
const userToDelete = ref(null);
|
|
||||||
const selectedUserForSubscription = ref(null);
|
|
||||||
const loadingSubscription = ref(false);
|
|
||||||
const userSubscriptions = ref({});
|
|
||||||
const availablePlans = ref([]);
|
|
||||||
const subscriptionError = ref('');
|
|
||||||
const subscriptionSuccess = ref('');
|
|
||||||
const addError = ref('');
|
|
||||||
|
|
||||||
const userForm = ref({
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
passwordConfirm: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const subscriptionForm = ref({
|
|
||||||
planId: 'free',
|
|
||||||
status: 'active',
|
|
||||||
cancelAtPeriodEnd: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedUserSubscription = ref(null);
|
|
||||||
|
|
||||||
const isAuthenticated = computed(() => authService.hasCredentials());
|
|
||||||
const currentUser = computed(() => {
|
|
||||||
return authService.getUsername() || '';
|
|
||||||
});
|
|
||||||
const isAdmin = computed(() => {
|
|
||||||
return authService.isAdmin();
|
|
||||||
});
|
|
||||||
|
|
||||||
function formatDate(dateString) {
|
|
||||||
if (!dateString) return 'N/A';
|
|
||||||
try {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleString('es-ES', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
return dateString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUsers() {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const data = await api.getUsers();
|
|
||||||
users.value = data.users || [];
|
|
||||||
|
|
||||||
// Cargar información de suscripción para todos los usuarios (solo si es admin)
|
|
||||||
if (isAdmin.value) {
|
|
||||||
await loadAllUserSubscriptions();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error cargando usuarios:', error);
|
|
||||||
// El modal de login se manejará automáticamente desde App.vue
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAllUserSubscriptions() {
|
|
||||||
// Cargar suscripciones de todos los usuarios en paralelo
|
|
||||||
const subscriptionPromises = users.value.map(async (user) => {
|
|
||||||
try {
|
|
||||||
// Usar el endpoint de admin para obtener suscripción de cualquier usuario
|
|
||||||
const response = await fetch(`/api/subscription/${user.username}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': authService.getAuthHeader(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
userSubscriptions.value[user.username] = data;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error cargando suscripción de ${user.username}:`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await Promise.all(subscriptionPromises);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAvailablePlans() {
|
|
||||||
try {
|
|
||||||
const data = await api.getSubscriptionPlans();
|
|
||||||
availablePlans.value = data.plans || [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error cargando planes:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openSubscriptionModal(username) {
|
|
||||||
selectedUserForSubscription.value = username;
|
|
||||||
showSubscriptionModal.value = true;
|
|
||||||
loadingSubscription.value = true;
|
|
||||||
subscriptionError.value = '';
|
|
||||||
subscriptionSuccess.value = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Cargar planes disponibles
|
|
||||||
await loadAvailablePlans();
|
|
||||||
|
|
||||||
// Cargar suscripción del usuario seleccionado
|
|
||||||
const response = await fetch(`/api/subscription/${username}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': authService.getAuthHeader(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
selectedUserSubscription.value = data;
|
|
||||||
subscriptionForm.value = {
|
|
||||||
planId: data.subscription?.planId || 'free',
|
|
||||||
status: data.subscription?.status || 'active',
|
|
||||||
cancelAtPeriodEnd: data.subscription?.cancelAtPeriodEnd || false,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Si no hay suscripción, usar valores por defecto
|
|
||||||
selectedUserSubscription.value = {
|
|
||||||
subscription: {
|
|
||||||
planId: 'free',
|
|
||||||
status: 'active',
|
|
||||||
plan: { name: 'Gratis', description: 'Plan gratuito' }
|
|
||||||
},
|
|
||||||
usage: { workers: 0, maxWorkers: 2 },
|
|
||||||
};
|
|
||||||
subscriptionForm.value = {
|
|
||||||
planId: 'free',
|
|
||||||
status: 'active',
|
|
||||||
cancelAtPeriodEnd: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error cargando suscripción:', error);
|
|
||||||
subscriptionError.value = 'Error al cargar información de suscripción';
|
|
||||||
} finally {
|
|
||||||
loadingSubscription.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeSubscriptionModal() {
|
|
||||||
showSubscriptionModal.value = false;
|
|
||||||
selectedUserForSubscription.value = null;
|
|
||||||
selectedUserSubscription.value = null;
|
|
||||||
subscriptionError.value = '';
|
|
||||||
subscriptionSuccess.value = '';
|
|
||||||
subscriptionForm.value = {
|
|
||||||
planId: 'free',
|
|
||||||
status: 'active',
|
|
||||||
cancelAtPeriodEnd: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleUpdateUserSubscription() {
|
|
||||||
if (!selectedUserForSubscription.value) return;
|
|
||||||
|
|
||||||
subscriptionError.value = '';
|
|
||||||
subscriptionSuccess.value = '';
|
|
||||||
loadingAction.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.updateUserSubscription(selectedUserForSubscription.value, {
|
|
||||||
planId: subscriptionForm.value.planId,
|
|
||||||
status: subscriptionForm.value.status,
|
|
||||||
cancelAtPeriodEnd: subscriptionForm.value.cancelAtPeriodEnd,
|
|
||||||
});
|
|
||||||
|
|
||||||
subscriptionSuccess.value = 'Suscripción actualizada correctamente';
|
|
||||||
|
|
||||||
// Actualizar la información en la lista
|
|
||||||
await loadAllUserSubscriptions();
|
|
||||||
|
|
||||||
// Recargar la información del modal después de un momento
|
|
||||||
setTimeout(async () => {
|
|
||||||
await openSubscriptionModal(selectedUserForSubscription.value);
|
|
||||||
}, 1000);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error actualizando suscripción:', error);
|
|
||||||
subscriptionError.value = error.response?.data?.error || 'Error al actualizar la suscripción';
|
|
||||||
} finally {
|
|
||||||
loadingAction.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCreateUser() {
|
|
||||||
addError.value = '';
|
|
||||||
loadingAction.value = true;
|
|
||||||
|
|
||||||
if (!userForm.value.username || !userForm.value.password || !userForm.value.passwordConfirm) {
|
|
||||||
addError.value = 'Todos los campos son requeridos';
|
|
||||||
loadingAction.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userForm.value.username.length < 3) {
|
|
||||||
addError.value = 'El nombre de usuario debe tener al menos 3 caracteres';
|
|
||||||
loadingAction.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userForm.value.password.length < 6) {
|
|
||||||
addError.value = 'La contraseña debe tener al menos 6 caracteres';
|
|
||||||
loadingAction.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userForm.value.password !== userForm.value.passwordConfirm) {
|
|
||||||
addError.value = 'Las contraseñas no coinciden';
|
|
||||||
loadingAction.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.createUser({
|
|
||||||
username: userForm.value.username,
|
|
||||||
password: userForm.value.password,
|
|
||||||
});
|
|
||||||
|
|
||||||
closeAddModal();
|
|
||||||
await loadUsers();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creando usuario:', error);
|
|
||||||
if (error.response?.data?.error) {
|
|
||||||
addError.value = error.response.data.error;
|
|
||||||
} else {
|
|
||||||
addError.value = 'Error creando usuario. Intenta de nuevo.';
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
loadingAction.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function handleDeleteUser() {
|
|
||||||
if (!userToDelete.value) return;
|
|
||||||
|
|
||||||
loadingAction.value = true;
|
|
||||||
try {
|
|
||||||
await api.deleteUser(userToDelete.value);
|
|
||||||
userToDelete.value = null;
|
|
||||||
await loadUsers();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error eliminando usuario:', error);
|
|
||||||
alert(error.response?.data?.error || 'Error eliminando usuario. Intenta de nuevo.');
|
|
||||||
} finally {
|
|
||||||
loadingAction.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmDeleteUser(username) {
|
|
||||||
userToDelete.value = username;
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeAddModal() {
|
|
||||||
showAddModal.value = false;
|
|
||||||
addError.value = '';
|
|
||||||
userForm.value = {
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
passwordConfirm: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAuthLogout() {
|
|
||||||
// Cuando el usuario se desconecta globalmente, limpiar datos
|
|
||||||
users.value = [];
|
|
||||||
showAddModal.value = false;
|
|
||||||
userToDelete.value = null;
|
|
||||||
addError.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadUsers();
|
|
||||||
window.addEventListener('auth-logout', handleAuthLogout);
|
|
||||||
// Escuchar evento de login exitoso para recargar usuarios
|
|
||||||
window.addEventListener('auth-login', () => {
|
|
||||||
loadUsers();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('auth-logout', handleAuthLogout);
|
|
||||||
window.removeEventListener('auth-login', loadUsers);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
@@ -20,6 +20,9 @@ FROM nginx:alpine
|
|||||||
# Copiar archivos construidos
|
# Copiar archivos construidos
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html/landing
|
COPY --from=builder /app/dist /usr/share/nginx/html/landing
|
||||||
|
|
||||||
|
#change /usr/share/nginx/html to /usr/share/nginx/html/landing
|
||||||
|
RUN sed -i 's|/usr/share/nginx/html|/usr/share/nginx/html/landing|g' /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
# Exponer puerto
|
# Exponer puerto
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user