From 02b3ae8334c5781c025452f9fc7b8a229c9c68f8 Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Fri, 15 May 2026 17:32:08 -0500 Subject: [PATCH] Add permissions hydration handling to improve user experience and prevent access flash - Introduced `permissionsHydrated` writable store to track if RBAC permissions are loaded for the active company. - Added `markPermissionsHydrated` function to set the hydration state. - Updated dashboard components to wait for permissions to be hydrated before rendering restricted content, enhancing user experience by avoiding "Access Denied" flashes. - Refactored permission checks in `pedimento-permissions.ts` to consistently use `userHasPermission` for clarity and maintainability. --- frontend/src/lib/auth.ts | 17 ++++++ .../lib/permissions/pedimento-permissions.ts | 15 ++--- frontend/src/routes/dashboard/+layout.svelte | 7 ++- .../routes/dashboard/pedimentos/+page.svelte | 55 ++++++++++--------- .../pedimentos/edit/[id]/+page.svelte | 11 +++- 5 files changed, 65 insertions(+), 40 deletions(-) diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 288f50ad..31f2f55c 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -150,6 +150,19 @@ export const authStore = createAuthStore(); export const isAuthenticated = derived(authStore, ($a) => $a.isAuthenticated); export const currentUser = derived(authStore, ($a) => $a.user); +/** + * Indica si los permisos RBAC del usuario para la compañía activa ya se hidrataron + * en cliente (vía {@link syncCompanyPermissions} o tras la inicialización del + * dashboard si no hay compañía). Las pantallas que muestran 403 según permisos + * deben esperar a que esto sea `true` antes de decidir, para evitar el flash + * de "Acceso restringido" en el primer render. + */ +export const permissionsHydrated = writable(false); + +export function markPermissionsHydrated(): void { + permissionsHydrated.set(true); +} + /** * Verifica si el usuario tiene un permiso específico. * `user.permissions` debe incluir códigos de la app (p. ej. `user.view`); suelen @@ -441,6 +454,10 @@ export async function syncCompanyPermissions(companyId: number): Promise { }); } catch (e) { console.warn('[auth] syncCompanyPermissions:', e); + } finally { + // Levanta el flag aunque la sync falle: si no se pudo, las pantallas + // quedan con lo que vino del SSR y deben dejar de mostrar el loader. + permissionsHydrated.set(true); } } diff --git a/frontend/src/lib/permissions/pedimento-permissions.ts b/frontend/src/lib/permissions/pedimento-permissions.ts index 7d28799f..42b2ef81 100644 --- a/frontend/src/lib/permissions/pedimento-permissions.ts +++ b/frontend/src/lib/permissions/pedimento-permissions.ts @@ -1,34 +1,29 @@ /** * Permisos de pedimentos — códigos `pedimentos_mgmt.*` (seed / API). * - * - **Ver listado:** `userHasPermission` (incluye comodín de rol Keycloak `admin` como en otras pantallas). - * - **Crear / editar / borrar (mutaciones):** solo códigos en `user.permissions` (tras `syncCompanyPermissions`). + * Todas las comprobaciones usan `userHasPermission`, que respeta el comodín de + * rol Keycloak `admin` (mismo patrón que `invoice-permissions.ts`). * * **Alta de pedimento (`Nuevo` / `/edit/new`):** requiere `pedimentos_mgmt.create`. */ import type { User } from '$lib/auth'; import { userHasPermission } from '$lib/auth'; -function hasAssignedCode(user: User | null, code: string): boolean { - if (!user) return false; - return user.permissions.includes(code); -} - export function canViewPedimentosList(user: User | null): boolean { return userHasPermission(user, 'pedimentos_mgmt.view'); } /** Permiso de API/rol para crear pedimento. */ export function canCreatePedimento(user: User | null): boolean { - return hasAssignedCode(user, 'pedimentos_mgmt.create'); + return userHasPermission(user, 'pedimentos_mgmt.create'); } export function canEditPedimento(user: User | null): boolean { - return hasAssignedCode(user, 'pedimentos_mgmt.edit'); + return userHasPermission(user, 'pedimentos_mgmt.edit'); } export function canDeletePedimento(user: User | null): boolean { - return hasAssignedCode(user, 'pedimentos_mgmt.delete'); + return userHasPermission(user, 'pedimentos_mgmt.delete'); } /** Botón "Nuevo Pedimento" y navegación a `/edit/new` — requiere permiso create. */ diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index f1a7706d..4c04a394 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -18,7 +18,7 @@ SESSION_EXPIRED_EVENT } from '$lib/session-manager'; import type { SessionExpiredDetail } from '$lib/session-manager'; - import { authStore } from '$lib/auth'; + import { authStore, markPermissionsHydrated } from '$lib/auth'; import { logout, getKeycloakInstance } from '$lib/auth'; import LicenseErrorScreen from '$lib/components/license-error-screen.svelte'; @@ -140,7 +140,12 @@ await companyStore.initialize(data.companies, activeCompanyId); // Releer en cliente: el SSR puede haber quedado desfasado respecto al provisionamiento. await companyStore.loadCompanies(undefined, activeCompanyId); + // Garantiza que el flag se levante incluso si no hay compañía activa + // (en ese caso `syncCompanyPermissions` nunca corre). + markPermissionsHydrated(); })(); + } else { + markPermissionsHydrated(); } // ── Escuchar cambios de compañía y recargar datos ───────────────────── diff --git a/frontend/src/routes/dashboard/pedimentos/+page.svelte b/frontend/src/routes/dashboard/pedimentos/+page.svelte index ec31df6b..ef34e2f0 100644 --- a/frontend/src/routes/dashboard/pedimentos/+page.svelte +++ b/frontend/src/routes/dashboard/pedimentos/+page.svelte @@ -12,7 +12,7 @@ import { browser } from '$app/environment'; import { getAccessTokenFromDocument } from '$lib/access-token-cookie-browser'; import { companyStore } from '$lib/stores/company.svelte'; - import { Edit, Send, Plus, Trash2, RefreshCw } from 'lucide-svelte'; + import { Edit, Send, Plus, Trash2, RefreshCw, LoaderCircle } from 'lucide-svelte'; import { obtenerAtajosListaPedimento } from '$lib/config/shortcuts/dashboard/pedimentos/list'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { goto } from '$app/navigation'; @@ -22,7 +22,7 @@ import PdfProgressDialog from '$lib/components/dashboard/invoices/pdf-progress-dialog.svelte'; import { Checkbox } from '$lib/components/ui/checkbox'; import { get } from 'svelte/store'; - import { currentUser } from '$lib/auth'; + import { currentUser, permissionsHydrated } from '$lib/auth'; import ErrorState from '$lib/components/dashboard/common/error-state.svelte'; import { canViewPedimentosList, @@ -36,8 +36,6 @@ const canViewList = $derived(canViewPedimentosList($currentUser)); const canNuevoPedimentoAction = $derived(canOpenNuevoPedimentoForm($currentUser)); - const canEditPedimentoAction = $derived(canEditPedimento($currentUser)); - const canDeletePedimentoAction = $derived(canDeletePedimento($currentUser)); // Estado para filtros let filters = $state({ @@ -509,7 +507,14 @@ let filters = $state({ const columns = createColumns(handleSuccess); -{#if !canViewList} +{#if !$permissionsHydrated} +
+ +

Verificando permisos…

+
+{:else if !canViewList}
@@ -620,28 +625,24 @@ let filters = $state({
- {#if canEditPedimentoAction} - - {/if} - {#if canDeletePedimentoAction} - - {/if} + +