fix(dashboard): evitar flash de "Acceso Restringido" al refrescar

Sembrar authStore desde data.user en el script de +layout.svelte
(SSR + hidratación) en lugar de en onMount, para que $currentUser
ya esté poblado en el primer render de las páginas hijas. Antes,
canView quedaba en false durante el SSR/hidratación y las páginas
renderizaban ErrorState 403 hasta que onMount llenaba el store.

Aplica al refrescar la página (F5) y al botón "Refrescar" de los
módulos que usan window.location.reload (countries, material_types,
payment_methods, document_types_digitization, clients_and_providers).

No se modificaron permisos, helpers de permisos, ni el patrón
canView/isError de las páginas afectadas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 13:07:57 -05:00
parent 76b03b2938
commit 75ea8a347a

View File

@@ -61,6 +61,48 @@
// Actualizar el contexto reactive al cambiar data.user (post invalidateAll)
$effect(() => { setContext('user', data.user); });
// ── Sembrar el authStore desde data.user ───────────────────────────────
// Se ejecuta SÍNCRONAMENTE en el script (SSR + hidratación), no en onMount,
// para que `$currentUser` ya esté poblado en el primer render de las páginas
// hijas. Evita el flash de "Acceso restringido" al refrescar cuando las páginas
// derivan `canView` a partir de `$currentUser`.
function syncAuthStoreFromData() {
if (!data.user) return;
if (data.user.token) {
authStore.setToken(data.user.token);
authStore.setAuthenticated(true);
}
const resolvedAvatarUrl =
data.user.workspaceAvatarUrl ||
data.user.workspace_avatar_url ||
data.user.avatarUrl ||
data.user.avatar_url ||
data.user.legacyAvatarUrl ||
data.user.legacy_avatar_url ||
null;
authStore.setUser({
id: data.user.sub ?? data.user.id ?? '',
username: data.user.preferred_username ?? data.user.username ?? '',
email: data.user.email,
name: data.user.name,
avatarUrl: resolvedAvatarUrl,
workspaceAvatarUrl: data.user.workspaceAvatarUrl ?? data.user.workspace_avatar_url ?? null,
legacyAvatarUrl: data.user.legacyAvatarUrl ?? data.user.legacy_avatar_url ?? data.user.avatar_url ?? null,
tenantId: data.user.tenant_id,
roles: data.user.roles ?? [],
permissions: data.user.permissions ?? []
});
}
syncAuthStoreFromData();
// Re-sincronizar si data.user cambia (por ejemplo, tras invalidateAll o cambio de compañía)
$effect(() => {
// Dependencia explícita para que el effect reaccione al swap de data.user
data.user;
syncAuthStoreFromData();
});
// ── Manejar expiración de sesión ────────────────────────────────────────
function handleSessionExpired(e: Event) {
const { reason } = (e as CustomEvent<SessionExpiredDetail>).detail;
@@ -69,37 +111,6 @@
}
onMount(() => {
// ── Inicializar el token en el authStore desde los datos del servidor ──
// El servidor valida la cookie y pasa el access_token a través de data.user.token.
// Lo almacenamos en memoria (authStore) sin tocar localStorage.
if (data.user?.token) {
authStore.setToken(data.user.token);
authStore.setAuthenticated(true);
}
if (data.user) {
const resolvedAvatarUrl =
data.user.workspaceAvatarUrl ||
data.user.workspace_avatar_url ||
data.user.avatarUrl ||
data.user.avatar_url ||
data.user.legacyAvatarUrl ||
data.user.legacy_avatar_url ||
null;
authStore.setUser({
id: data.user.sub ?? data.user.id ?? '',
username: data.user.preferred_username ?? data.user.username ?? '',
email: data.user.email,
name: data.user.name,
avatarUrl: resolvedAvatarUrl,
workspaceAvatarUrl: data.user.workspaceAvatarUrl ?? data.user.workspace_avatar_url ?? null,
legacyAvatarUrl: data.user.legacyAvatarUrl ?? data.user.legacy_avatar_url ?? data.user.avatar_url ?? null,
tenantId: data.user.tenant_id,
roles: data.user.roles ?? [],
permissions: data.user.permissions ?? []
});
}
// ── Inicializar el SessionManager ─────────────────────────────────────
if (data.user?.token) {
const mgr = createSessionManager({