feat: implement authentication flow with login page and router setup

This commit is contained in:
Edgar Mendez Mendoza 2025-11-06 11:46:13 -06:00
parent 8941568e08
commit 83835c22a5
10 changed files with 448 additions and 24 deletions

5
components.d.ts vendored
View File

@ -16,11 +16,16 @@ declare module 'vue' {
Badge: typeof import('primevue/badge')['default']
Button: typeof import('primevue/button')['default']
Card: typeof import('primevue/card')['default']
Checkbox: typeof import('primevue/checkbox')['default']
Column: typeof import('primevue/column')['default']
DataTable: typeof import('primevue/datatable')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
InputGroup: typeof import('primevue/inputgroup')['default']
InputGroupAddon: typeof import('primevue/inputgroupaddon')['default']
InputText: typeof import('primevue/inputtext')['default']
KpiCard: typeof import('./src/components/shared/KpiCard.vue')['default']
Menu: typeof import('primevue/menu')['default']
Message: typeof import('primevue/message')['default']
Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default']
Tag: typeof import('primevue/tag')['default']
TopBar: typeof import('./src/components/layout/TopBar.vue')['default']

24
package-lock.json generated
View File

@ -16,7 +16,8 @@
"primevue": "^4.4.1",
"tailwindcss-primeui": "^0.6.1",
"unplugin-vue-components": "^30.0.0",
"vue": "^3.5.22"
"vue": "^3.5.22",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@types/node": "^24.6.0",
@ -1291,6 +1292,12 @@
"@vue/shared": "3.5.23"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/language-core": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.3.tgz",
@ -2363,6 +2370,21 @@
}
}
},
"node_modules/vue-router": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz",
"integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/vue-tsc": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.3.tgz",

View File

@ -17,7 +17,8 @@
"primevue": "^4.4.1",
"tailwindcss-primeui": "^0.6.1",
"unplugin-vue-components": "^30.0.0",
"vue": "^3.5.22"
"vue": "^3.5.22",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@types/node": "^24.6.0",

View File

@ -1,14 +1,8 @@
<script setup lang="ts">
import { ref } from 'vue';
import ColorDemo from './ColorDemo.vue';
import MainLayout from './MainLayout.vue';
// Cambiar entre 'color' y 'warehouse' para ver diferentes demos
const currentView = ref<'color' | 'warehouse'>('warehouse');
// El router se encarga de manejar las vistas
</script>
<template>
<ColorDemo v-if="currentView === 'color'" />
<MainLayout v-else />
<RouterView />
</template>

View File

@ -1,7 +1,6 @@
<script setup lang="ts">
import TopBar from './components/layout/TopBar.vue';
import Sidebar from './components/layout/Sidebar.vue';
import WarehouseDashboard from './modules/warehouse/components/WarehouseDashboard.vue';
</script>
<template>
@ -16,7 +15,7 @@ import WarehouseDashboard from './modules/warehouse/components/WarehouseDashboar
<!-- Page Content -->
<main class="flex-1 overflow-auto p-4 lg:p-6">
<WarehouseDashboard />
<RouterView />
</main>
</div>
</div>

View File

@ -1,9 +1,12 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useLayout } from "../../composables/useLayout";
import AppConfig from "./AppConfig.vue";
import { useAuth } from "../../stores/auth";
const router = useRouter();
const { isDarkMode, toggleDarkMode } = useLayout();
const { user, logout } = useAuth();
// Referencia al menú de usuario
const userMenu = ref();
@ -13,22 +16,26 @@ const toggleUserMenu = (event: Event) => {
userMenu.value.toggle(event);
};
// Función de logout
const handleLogout = () => {
logout();
router.push('/login');
};
// Opciones del menú de usuario
const userMenuItems = ref([
{
label: 'Mi Perfil',
icon: 'pi pi-user',
command: () => {
console.log('Ir a perfil');
// Aquí puedes agregar la navegación: router.push('/profile')
router.push('/profile');
}
},
{
label: 'Configuración',
icon: 'pi pi-cog',
command: () => {
console.log('Ir a configuración');
// Aquí puedes agregar la navegación: router.push('/settings')
router.push('/settings');
}
},
{
@ -38,9 +45,7 @@ const userMenuItems = ref([
label: 'Cerrar Sesión',
icon: 'pi pi-sign-out',
command: () => {
console.log('Cerrar sesión');
// Aquí puedes agregar la lógica de logout
// Por ejemplo: authStore.logout() o router.push('/login')
handleLogout();
}
}
]);
@ -124,10 +129,10 @@ const userMenuItems = ref([
aria-controls="user_menu"
>
<div class="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white text-sm font-semibold">
JD
{{ user?.name.split(' ').map(n => n[0]).join('').toUpperCase() || 'U' }}
</div>
<span class="text-sm font-medium text-surface-900 dark:text-surface-0 hidden md:block">
John Doe
{{ user?.name || 'Usuario' }}
</span>
<i class="pi pi-angle-down text-sm text-surface-500 hidden md:block"></i>
</button>
@ -142,8 +147,8 @@ const userMenuItems = ref([
>
<template #start>
<div class="px-4 py-3 border-b border-surface-200 dark:border-surface-700">
<p class="text-sm font-semibold text-surface-900 dark:text-surface-0">John Doe</p>
<p class="text-xs text-surface-500 dark:text-surface-400">john.doe@example.com</p>
<p class="text-sm font-semibold text-surface-900 dark:text-surface-0">{{ user?.name }}</p>
<p class="text-xs text-surface-500 dark:text-surface-400">{{ user?.email }}</p>
</div>
</template>
<template #item="{ item, props }">

View File

@ -6,6 +6,8 @@ import PrimeVue from "primevue/config";
import StyleClass from "primevue/styleclass";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { useAuth } from "./stores/auth";
// Crear un preset personalizado basado en Aura con color azul
const MyPreset = definePreset(Aura, {
@ -28,6 +30,11 @@ const MyPreset = definePreset(Aura, {
const app = createApp(App);
// Inicializar autenticación desde localStorage
const { initAuth } = useAuth();
initAuth();
app.use(router);
app.use(PrimeVue, {
theme: {
preset: MyPreset,

210
src/pages/Auth/Login.vue Normal file
View File

@ -0,0 +1,210 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useAuth } from '../../stores/auth';
import { useRouter } from 'vue-router';
const router = useRouter();
const { login, isLoading } = useAuth();
const email = ref('');
const password = ref('');
const remember = ref(false);
const showPassword = ref(false);
const errorMessage = ref('');
const isFormValid = computed(() => {
return email.value.trim() !== '' && password.value.trim() !== '';
});
const handleLogin = async () => {
errorMessage.value = '';
const result = await login({
email: email.value,
password: password.value
});
if (result.success) {
router.push('/');
} else {
errorMessage.value = result.error || 'Error al iniciar sesión';
}
};
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Enter' && isFormValid.value) {
handleLogin();
}
};
</script>
<template>
<div class="min-h-screen flex items-center justify-center bg-surface-50 dark:bg-surface-950 p-4">
<div class="w-full max-w-md">
<!-- Card de Login -->
<div class="bg-surface-0 dark:bg-surface-900 rounded-2xl shadow-xl border border-surface-200 dark:border-surface-800 overflow-hidden">
<!-- Header con logo y título -->
<div class="bg-linear-to-br from-primary to-primary-600 p-8 text-center">
<div class="flex items-center justify-center mb-4">
<div class="w-16 h-16 bg-white/20 backdrop-blur-sm rounded-2xl flex items-center justify-center">
<i class="pi pi-chart-line text-4xl text-white"></i>
</div>
</div>
<h1 class="text-3xl font-bold text-white mb-2">GOLS Control</h1>
<p class="text-primary-100">Sistema de Gestión Empresarial</p>
</div>
<!-- Formulario -->
<div class="p-8">
<h2 class="text-2xl font-semibold text-surface-900 dark:text-surface-0 mb-2">
Iniciar Sesión
</h2>
<p class="text-surface-600 dark:text-surface-400 mb-6">
Ingresa tus credenciales para continuar
</p>
<!-- Mensaje de error -->
<Message
v-if="errorMessage"
severity="error"
:closable="false"
class="mb-4"
>
{{ errorMessage }}
</Message>
<!-- Info de prueba -->
<Message severity="info" :closable="false" class="mb-6">
<div class="text-sm">
<p class="font-semibold mb-1">Credenciales de prueba:</p>
<p><strong>Email:</strong> admin@gols.com</p>
<p><strong>Password:</strong> admin123</p>
</div>
</Message>
<form @submit.prevent="handleLogin" class="space-y-6">
<!-- Email -->
<div class="space-y-2">
<label for="email" class="block text-sm font-medium text-surface-700 dark:text-surface-300">
Correo Electrónico
</label>
<InputGroup>
<InputGroupAddon>
<i class="pi pi-envelope"></i>
</InputGroupAddon>
<InputText
id="email"
v-model="email"
type="email"
placeholder="tu@email.com"
:disabled="isLoading"
@keypress="handleKeyPress"
class="w-full"
autocomplete="email"
/>
</InputGroup>
</div>
<!-- Password -->
<div class="space-y-2">
<label for="password" class="block text-sm font-medium text-surface-700 dark:text-surface-300">
Contraseña
</label>
<InputGroup>
<InputGroupAddon>
<i class="pi pi-lock"></i>
</InputGroupAddon>
<InputText
id="password"
v-model="password"
:type="showPassword ? 'text' : 'password'"
placeholder="••••••••"
:disabled="isLoading"
@keypress="handleKeyPress"
class="w-full"
autocomplete="current-password"
/>
<InputGroupAddon class="cursor-pointer" @click="showPassword = !showPassword">
<i :class="showPassword ? 'pi pi-eye-slash' : 'pi pi-eye'"></i>
</InputGroupAddon>
</InputGroup>
</div>
<!-- Recordar sesión -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Checkbox
inputId="remember"
v-model="remember"
:binary="true"
/>
<label for="remember" class="text-sm text-surface-700 dark:text-surface-300 cursor-pointer">
Recordar sesión
</label>
</div>
<a href="#" class="text-sm text-primary hover:text-primary-700 font-medium">
¿Olvidaste tu contraseña?
</a>
</div>
<!-- Botón de login -->
<Button
type="submit"
label="Iniciar Sesión"
icon="pi pi-sign-in"
:loading="isLoading"
:disabled="!isFormValid || isLoading"
class="w-full"
size="large"
/>
</form>
<!-- Separador -->
<div class="relative my-6">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-surface-300 dark:border-surface-700"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-4 bg-surface-0 dark:bg-surface-900 text-surface-500 dark:text-surface-400">
O continuar con
</span>
</div>
</div>
<!-- Botones sociales -->
<div class="grid grid-cols-2 gap-3">
<Button
label="Google"
icon="pi pi-google"
outlined
severity="secondary"
class="w-full"
/>
<Button
label="Microsoft"
icon="pi pi-microsoft"
outlined
severity="secondary"
class="w-full"
/>
</div>
</div>
<!-- Footer -->
<div class="px-8 py-6 bg-surface-50 dark:bg-surface-950 border-t border-surface-200 dark:border-surface-800 text-center">
<p class="text-sm text-surface-600 dark:text-surface-400">
¿No tienes una cuenta?
<a href="#" class="text-primary hover:text-primary-700 font-medium">
Solicitar acceso
</a>
</p>
</div>
</div>
<!-- Copyright -->
<p class="text-center text-sm text-surface-500 dark:text-surface-400 mt-8">
© 2025 GOLS Control. Todos los derechos reservados.
</p>
</div>
</div>
</template>

88
src/router/index.ts Normal file
View File

@ -0,0 +1,88 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
import { useAuth } from '../stores/auth';
// Importar vistas
import Login from '../pages/Auth/Login.vue';
import MainLayout from '../MainLayout.vue';
import WarehouseDashboard from '../modules/warehouse/components/WarehouseDashboard.vue';
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: Login,
meta: {
requiresAuth: false,
title: 'Iniciar Sesión'
}
},
{
path: '/',
component: MainLayout,
meta: {
requiresAuth: true
},
children: [
{
path: '',
name: 'Dashboard',
component: WarehouseDashboard,
meta: {
title: 'Dashboard',
requiresAuth: true
}
},
{
path: 'warehouse',
name: 'Warehouse',
redirect: '/warehouse/inventory',
children: [
{
path: 'inventory',
name: 'WarehouseInventory',
component: WarehouseDashboard,
meta: {
title: 'Inventario',
requiresAuth: true
}
}
]
}
]
},
{
path: '/:pathMatch(.*)*',
redirect: '/'
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
// Navigation Guard
router.beforeEach((to, _from, next) => {
const { isAuthenticated } = useAuth();
// Actualizar título de la página
document.title = to.meta.title
? `${to.meta.title} - GOLS Control`
: 'GOLS Control';
// Verificar si la ruta requiere autenticación
if (to.meta.requiresAuth && !isAuthenticated.value) {
// Redirigir al login si no está autenticado
next({
name: 'Login',
query: { redirect: to.fullPath } // Guardar ruta para redireccionar después del login
});
} else if (to.name === 'Login' && isAuthenticated.value) {
// Si ya está autenticado y va al login, redirigir al dashboard
next({ name: 'Dashboard' });
} else {
next();
}
});
export default router;

93
src/stores/auth.ts Normal file
View File

@ -0,0 +1,93 @@
import { ref, computed } from 'vue';
export interface User {
id: number;
name: string;
email: string;
avatar?: string;
role: string;
}
export interface LoginCredentials {
email: string;
password: string;
}
// Estado global de autenticación
const user = ref<User | null>(null);
const token = ref<string | null>(null);
const isLoading = ref(false);
// Inicializar desde localStorage
const initAuth = () => {
const storedToken = localStorage.getItem('auth_token');
const storedUser = localStorage.getItem('auth_user');
if (storedToken && storedUser) {
token.value = storedToken;
user.value = JSON.parse(storedUser);
}
};
// Computeds
const isAuthenticated = computed(() => !!user.value && !!token.value);
// Función de login (simulada)
const login = async (credentials: LoginCredentials): Promise<{ success: boolean; error?: string }> => {
isLoading.value = true;
try {
// Simular llamada a API
await new Promise(resolve => setTimeout(resolve, 1000));
// Validación simple (en producción esto vendría del backend)
if (credentials.email === 'admin@gols.com' && credentials.password === 'admin123') {
const mockUser: User = {
id: 1,
name: 'John Doe',
email: credentials.email,
avatar: '',
role: 'admin'
};
const mockToken = 'mock-jwt-token-' + Date.now();
// Guardar en estado
user.value = mockUser;
token.value = mockToken;
// Persistir en localStorage
localStorage.setItem('auth_token', mockToken);
localStorage.setItem('auth_user', JSON.stringify(mockUser));
return { success: true };
} else {
return { success: false, error: 'Credenciales inválidas' };
}
} catch (error) {
return { success: false, error: 'Error al iniciar sesión' };
} finally {
isLoading.value = false;
}
};
// Función de logout
const logout = () => {
user.value = null;
token.value = null;
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_user');
};
// Composable para usar en componentes
export const useAuth = () => {
return {
user,
token,
isLoading,
isAuthenticated,
login,
logout,
initAuth
};
};