feat: implement authentication flow with login page and router setup
This commit is contained in:
parent
8941568e08
commit
83835c22a5
5
components.d.ts
vendored
5
components.d.ts
vendored
@ -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
24
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
10
src/App.vue
10
src/App.vue
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 }">
|
||||
|
||||
@ -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
210
src/pages/Auth/Login.vue
Normal 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
88
src/router/index.ts
Normal 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
93
src/stores/auth.ts
Normal 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
|
||||
};
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user