Permiso vistas implementados
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { CLIENT_ROUTES } from '$lib/routes-registry';
|
||||
import { auth } from '$lib/stores/auth.js';
|
||||
import { navPerms } from '$lib/stores/navPermissions';
|
||||
import { onMount } from 'svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
|
||||
@@ -8,24 +10,29 @@
|
||||
|
||||
let isMenuOpen = false;
|
||||
|
||||
onMount(() => {
|
||||
auth.init();
|
||||
onMount(async () => {
|
||||
await auth.init();
|
||||
if ($auth.isAuthenticated) {
|
||||
await navPerms.load();
|
||||
}
|
||||
});
|
||||
|
||||
$: visibleRoutes = CLIENT_ROUTES.filter(r => $navPerms[r.key] === true);
|
||||
|
||||
function toggleMenu() {
|
||||
isMenuOpen = !isMenuOpen;
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
isMenuOpen = false;
|
||||
auth.logout(); // El store maneja la redirección automática
|
||||
navPerms.clear();
|
||||
auth.logout();
|
||||
}
|
||||
</script>
|
||||
|
||||
<header class="bg-white shadow-sm border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<!-- Logo -->
|
||||
{#if showLogo}
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="flex items-center space-x-2">
|
||||
@@ -40,22 +47,16 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Navigation -->
|
||||
{#if showNavigation && $auth.isAuthenticated}
|
||||
<nav class="hidden md:flex space-x-8">
|
||||
<a
|
||||
href="/tickets"
|
||||
class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium"
|
||||
>
|
||||
Mis Tickets
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/usuarios"
|
||||
class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium"
|
||||
>
|
||||
Usuarios
|
||||
</a>
|
||||
{#each visibleRoutes as route}
|
||||
<a
|
||||
href={route.path}
|
||||
class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium"
|
||||
>
|
||||
{route.label}
|
||||
</a>
|
||||
{/each}
|
||||
<a
|
||||
href="/tickets/new"
|
||||
class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium"
|
||||
@@ -65,7 +66,6 @@
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<!-- User menu -->
|
||||
<div class="flex items-center space-x-4">
|
||||
{#if $auth.isAuthenticated}
|
||||
<div class="relative">
|
||||
@@ -98,17 +98,13 @@
|
||||
<a
|
||||
href="/profile"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
on:click={() => (isMenuOpen = false)}
|
||||
on:click={() => (isMenuOpen = false)}>Mi Perfil</a
|
||||
>
|
||||
Mi Perfil
|
||||
</a>
|
||||
<a
|
||||
href="/organization"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
on:click={() => (isMenuOpen = false)}
|
||||
on:click={() => (isMenuOpen = false)}>Mi Organización</a
|
||||
>
|
||||
Mi Organización
|
||||
</a>
|
||||
<button
|
||||
on:click={handleLogout}
|
||||
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
@@ -130,23 +126,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile navigation -->
|
||||
{#if showNavigation && $auth.isAuthenticated}
|
||||
<div class="md:hidden border-t border-gray-200 py-2">
|
||||
<nav class="flex space-x-4">
|
||||
<a
|
||||
href="/tickets"
|
||||
class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium"
|
||||
>
|
||||
Mis Tickets
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/usuarios"
|
||||
class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium"
|
||||
>
|
||||
Usuarios
|
||||
</a>
|
||||
{#each visibleRoutes as route}
|
||||
<a
|
||||
href={route.path}
|
||||
class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium"
|
||||
>
|
||||
{route.label}
|
||||
</a>
|
||||
{/each}
|
||||
<a
|
||||
href="/tickets/new"
|
||||
class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium"
|
||||
@@ -159,7 +149,6 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Backdrop for mobile menu -->
|
||||
{#if isMenuOpen}
|
||||
<div class="fixed inset-0 z-40 md:hidden" on:click={() => (isMenuOpen = false)} />
|
||||
{/if}
|
||||
|
||||
11
frontend-client/src/lib/routes-registry.ts
Normal file
11
frontend-client/src/lib/routes-registry.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface RouteDefinition {
|
||||
key: string;
|
||||
label: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export const CLIENT_ROUTES: RouteDefinition[] = [
|
||||
{ key: 'nav:tickets', label: 'Mis Tickets', path: '/tickets' },
|
||||
{ key: 'nav:organization', label: 'Organización', path: '/organization' },
|
||||
{ key: 'nav:usuarios', label: 'Usuarios', path: '/usuarios' },
|
||||
]
|
||||
@@ -1,7 +1,6 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { auth } from './auth.js';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { auth } from './auth.js';
|
||||
|
||||
// Types
|
||||
export interface Category {
|
||||
@@ -60,15 +59,15 @@ function createAppStore() {
|
||||
// Load categories
|
||||
loadCategories: async () => {
|
||||
update(state => ({ ...state, isLoading: true, error: null }));
|
||||
|
||||
|
||||
try {
|
||||
const categories = await apiCall('/categories/');
|
||||
update(state => ({ ...state, categories, isLoading: false }));
|
||||
} catch (error) {
|
||||
update(state => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to load categories'
|
||||
update(state => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to load categories'
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
36
frontend-client/src/lib/stores/navPermissions.ts
Normal file
36
frontend-client/src/lib/stores/navPermissions.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { auth } from '$lib/stores/auth';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
function createNavPermissionsStore() {
|
||||
const { subscribe, set } = writable<Record<string, boolean>>({});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
load: async () => {
|
||||
try {
|
||||
const authState = get(auth);
|
||||
const slug = authState.user?.tenant_slug || authState.user?.tenant_id || '';
|
||||
const headers: Record<string, string> = {
|
||||
'X-App': 'client',
|
||||
'X-Tenant-Slug': slug,
|
||||
};
|
||||
if (authState.token) {
|
||||
headers['Authorization'] = `Bearer ${authState.token}`;
|
||||
}
|
||||
const res = await fetch('/api/v1/permissions/my', {
|
||||
credentials: 'include',
|
||||
headers
|
||||
});
|
||||
if (res.ok) {
|
||||
const perms = await res.json();
|
||||
set(perms);
|
||||
}
|
||||
} catch (e) {
|
||||
set({});
|
||||
}
|
||||
},
|
||||
clear: () => set({})
|
||||
};
|
||||
}
|
||||
|
||||
export const navPerms = createNavPermissionsStore();
|
||||
Reference in New Issue
Block a user