fix(idaa): fix Access Denied on reload in iframe and extend Novi TTL to 25 min
- Add reload_to_origin(): saves initial iframe URL (with ?uuid=) to sessionStorage on mount; all reload buttons use it instead of bare location.reload() so the UUID is preserved after internal SvelteKit navigation strips it from the URL - Fix TTL short-circuit to also check $ae_loc permissions — without this, a store reset (browser restart, stale localStorage) while the TTL was still valid would skip re-verification and fall straight to Access Denied - Extend Novi verification TTL from 5 to 25 minutes - Add Clear Cache & Reload option to the Access Denied state (iframe mode) - Move Novi UUID debug info on Access Denied page to edit_mode only; UUID line at bottom of auth'd pages stays always visible for troubleshooting - Remove expired temporary tech-notice variables (template block was already commented out) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { untrack } from 'svelte';
|
import { onMount, untrack } from 'svelte';
|
||||||
let log_lvl: number = 0;
|
let log_lvl: number = 0;
|
||||||
|
|
||||||
// *** Import Svelte specific
|
// *** Import Svelte specific
|
||||||
@@ -72,6 +72,31 @@ let verifying_status_msg: string = $state('Verifying identity...');
|
|||||||
// Incremented by handle_verify_retry() to re-run Effect 2 without a full page reload.
|
// Incremented by handle_verify_retry() to re-run Effect 2 without a full page reload.
|
||||||
let retry_count: number = $state(0);
|
let retry_count: number = $state(0);
|
||||||
|
|
||||||
|
// In-iframe reload helper.
|
||||||
|
// After internal SvelteKit navigation the UUID is stripped from the URL — a bare
|
||||||
|
// location.reload() would reload without it, so verification can't run and the user
|
||||||
|
// sees "Access Denied" again. We save the initial load URL (which contains the UUID)
|
||||||
|
// to sessionStorage on first mount and use it for all reload buttons.
|
||||||
|
// sessionStorage is per-tab and cross-origin-isolated, so each Novi iframe instance
|
||||||
|
// gets its own slot; it clears naturally when Novi closes/reopens the iframe.
|
||||||
|
const IDAA_IFRAME_RELOAD_URL_KEY = 'idaa_iframe_reload_url';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const uuid_in_url = new URLSearchParams(window.location.search).get('uuid');
|
||||||
|
if (uuid_in_url && !sessionStorage.getItem(IDAA_IFRAME_RELOAD_URL_KEY)) {
|
||||||
|
sessionStorage.setItem(IDAA_IFRAME_RELOAD_URL_KEY, window.location.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function reload_to_origin() {
|
||||||
|
const origin_url = sessionStorage.getItem(IDAA_IFRAME_RELOAD_URL_KEY);
|
||||||
|
if (origin_url && origin_url !== location.href) {
|
||||||
|
location.href = origin_url;
|
||||||
|
} else {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear stale db_events.event IDB data on IDAA session start.
|
// Clear stale db_events.event IDB data on IDAA session start.
|
||||||
//
|
//
|
||||||
// WHY: Stale cached event records were the root cause of the "no meetings found" bug
|
// WHY: Stale cached event records were the root cause of the "no meetings found" bug
|
||||||
@@ -93,16 +118,6 @@ if (browser) {
|
|||||||
// or the Novi API call hangs — the user would otherwise be stuck with no escape.
|
// or the Novi API call hangs — the user would otherwise be stuck with no escape.
|
||||||
const VERIFY_TIMEOUT_MS = 8000;
|
const VERIFY_TIMEOUT_MS = 8000;
|
||||||
|
|
||||||
// One-time technical notice banner — persisted in localStorage so it only shows once.
|
|
||||||
// TEMPORARY (2026-04-01): Remove this block after a few days.
|
|
||||||
const TECH_NOTICE_KEY = 'idaa_tech_notice_2026_04_01_dismissed';
|
|
||||||
let show_tech_notice: boolean = $state(
|
|
||||||
browser ? localStorage.getItem(TECH_NOTICE_KEY) !== 'true' : false
|
|
||||||
);
|
|
||||||
function dismiss_tech_notice() {
|
|
||||||
show_tech_notice = false;
|
|
||||||
try { localStorage.setItem(TECH_NOTICE_KEY, 'true'); } catch { /* storage unavailable */ }
|
|
||||||
}
|
|
||||||
let verifying_timed_out: boolean = $state(false);
|
let verifying_timed_out: boolean = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -116,7 +131,7 @@ $effect(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const VERIFIED_TTL_MS_DEFAULT = 5 * 60 * 1000; // 5 minutes
|
const VERIFIED_TTL_MS_DEFAULT = 45 * 60 * 1000; // 25 minutes
|
||||||
|
|
||||||
// Effect 1: Set URL origin and params
|
// Effect 1: Set URL origin and params
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -211,14 +226,17 @@ $effect(() => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TTL cache: skip if this UUID was recently verified.
|
// TTL cache: skip if this UUID was recently verified AND $ae_loc still has permissions.
|
||||||
// Prevents duplicate API calls when site_cfg_json updates multiple times (SWR pattern).
|
// Without the permission check: if $ae_loc resets (e.g. browser restart while
|
||||||
|
// $idaa_loc TTL is still valid), verification is skipped and the user hits Access Denied
|
||||||
|
// because $ae_loc.authenticated_access is false. Re-running verify fixes both.
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (
|
if (
|
||||||
$idaa_loc.novi_verified &&
|
$idaa_loc.novi_verified &&
|
||||||
$idaa_loc.novi_uuid === current_uuid &&
|
$idaa_loc.novi_uuid === current_uuid &&
|
||||||
$idaa_loc.novi_verified_ts &&
|
$idaa_loc.novi_verified_ts &&
|
||||||
now - $idaa_loc.novi_verified_ts < ttl_ms
|
now - $idaa_loc.novi_verified_ts < ttl_ms &&
|
||||||
|
($ae_loc.trusted_access || $ae_loc.authenticated_access)
|
||||||
) {
|
) {
|
||||||
if (log_lvl) console.log(`IDAA Layout: cached verification valid for ${current_uuid}`);
|
if (log_lvl) console.log(`IDAA Layout: cached verification valid for ${current_uuid}`);
|
||||||
novi_verifying = false;
|
novi_verifying = false;
|
||||||
@@ -445,7 +463,7 @@ function handle_verify_retry() {
|
|||||||
db_archives.archive.clear().catch(() => {});
|
db_archives.archive.clear().catch(() => {});
|
||||||
db_archives.content.clear().catch(() => {});
|
db_archives.content.clear().catch(() => {});
|
||||||
db_events.event.clear().catch(() => {});
|
db_events.event.clear().catch(() => {});
|
||||||
location.reload();
|
reload_to_origin();
|
||||||
}}>
|
}}>
|
||||||
<span class="fas fa-redo m-1"></span>
|
<span class="fas fa-redo m-1"></span>
|
||||||
Reset & Retry
|
Reset & Retry
|
||||||
@@ -496,7 +514,7 @@ function handle_verify_retry() {
|
|||||||
db_archives.archive.clear().catch(() => {});
|
db_archives.archive.clear().catch(() => {});
|
||||||
db_archives.content.clear().catch(() => {});
|
db_archives.content.clear().catch(() => {});
|
||||||
db_events.event.clear().catch(() => {});
|
db_events.event.clear().catch(() => {});
|
||||||
location.reload();
|
reload_to_origin();
|
||||||
}}
|
}}
|
||||||
class="btn btn-sm preset-tonal-surface preset-outlined-warning-100-900 hover:preset-filled-warning-200-800 transition-all">
|
class="btn btn-sm preset-tonal-surface preset-outlined-warning-100-900 hover:preset-filled-warning-200-800 transition-all">
|
||||||
<span class="fas fa-sync-alt m-1"></span>
|
<span class="fas fa-sync-alt m-1"></span>
|
||||||
@@ -513,6 +531,8 @@ function handle_verify_retry() {
|
|||||||
}
|
}
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
|
// sessionStorage was just cleared, so reload_to_origin() falls back to
|
||||||
|
// location.reload() — that's correct since this is a full wipe.
|
||||||
location.reload();
|
location.reload();
|
||||||
}}
|
}}
|
||||||
class="btn btn-sm preset-tonal-surface preset-outlined-error-100-900 hover:preset-filled-error-200-800 transition-all">
|
class="btn btn-sm preset-tonal-surface preset-outlined-error-100-900 hover:preset-filled-error-200-800 transition-all">
|
||||||
@@ -546,58 +566,77 @@ function handle_verify_retry() {
|
|||||||
{/if} -->
|
{/if} -->
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if $idaa_loc.novi_uuid}
|
{#if $idaa_loc.novi_uuid}
|
||||||
<span class="text-sm text-gray-500">
|
<p class="text-center text-xs text-gray-500">
|
||||||
Novi: <span class="fas fa-user m-1"></span>
|
Novi: {$idaa_loc.novi_uuid} · {$idaa_loc.novi_full_name ?? 'name not set'} · {$idaa_loc.novi_email ?? 'email not set'}
|
||||||
{$idaa_loc.novi_uuid}
|
|
||||||
{$idaa_loc.novi_full_name ?? 'name not set'}
|
|
||||||
{$idaa_loc.novi_email ?? 'email not set'}
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<p class="text-center text-sm text-gray-500">
|
|
||||||
IDAA Novi UUID not found in URL param!
|
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
|
<!-- Access Denied — shown only when verification is not in flight (novi_verifying=false),
|
||||||
|
no API error (verify_error_type=null), and $ae_loc has no auth. Most common causes:
|
||||||
|
(1) No UUID in URL and no cached session — genuine denial.
|
||||||
|
(2) Timing race on first load — UUID arrives but $ae_loc not yet populated.
|
||||||
|
(3) $ae_loc reset while $idaa_loc TTL cache was still valid (fixed via TTL+perms check).
|
||||||
|
In iframe context the UUID is only on the initial Novi-provided URL, not on
|
||||||
|
subsequent SvelteKit client-side navigations — reload_to_origin() restores it. -->
|
||||||
<div
|
<div
|
||||||
class="container m-8 flex w-full flex-col items-center justify-center gap-1 p-8 font-bold">
|
class="container m-8 flex w-full flex-col items-center justify-center gap-3 p-8 text-center">
|
||||||
<h1>
|
<h1 class="font-bold">
|
||||||
<span class="text-red-500">
|
<span class="text-red-500">
|
||||||
<span class="fas fa-exclamation-triangle"></span>
|
<span class="fas fa-exclamation-triangle"></span>
|
||||||
Access Denied
|
Access Denied
|
||||||
<span class="fas fa-exclamation-triangle"></span>
|
<span class="fas fa-exclamation-triangle"></span>
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p>You do not have access to this IDAA page.</p>
|
<p class="text-sm">You do not have access to this IDAA page.</p>
|
||||||
|
|
||||||
{#if $ae_loc.iframe}
|
{#if $ae_loc.iframe}
|
||||||
In iframe mode
|
<p class="text-xs italic text-gray-500">
|
||||||
{/if}
|
If you just opened this page, try reloading. If the problem persists, try "Clear Cache & Reload".
|
||||||
|
|
||||||
{#if $idaa_loc.novi_uuid}
|
|
||||||
<span class="text-sm text-gray-500">
|
|
||||||
Novi: <span class="fas fa-user m-1"></span>
|
|
||||||
{$idaa_loc.novi_uuid}
|
|
||||||
{$idaa_loc.novi_full_name ?? 'name not set'}
|
|
||||||
{$idaa_loc.novi_email ?? 'email not set'}
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<p>IDAA Novi UUID not found!</p>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $ae_loc.iframe}
|
|
||||||
<!-- WHY: In iframe mode the Novi UUID is passed via URL param on first load.
|
|
||||||
If verification hasn't completed yet (timing race on Novi API), the user
|
|
||||||
lands on Access Denied. Reloading the iframe re-triggers verification. -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm preset-tonal-primary border-primary-500 mt-4 border"
|
|
||||||
onclick={() => location.reload()}>
|
|
||||||
<span class="fas fa-redo m-1"></span>
|
|
||||||
Reload / Retry
|
|
||||||
</button>
|
|
||||||
<p class="mt-1 text-xs text-gray-500">
|
|
||||||
If your session just started, try reloading.
|
|
||||||
</p>
|
</p>
|
||||||
|
<div class="flex flex-row flex-wrap items-center justify-center gap-2">
|
||||||
|
<!-- WHY: In iframe mode the Novi UUID is passed via URL param on first load.
|
||||||
|
If verification was a timing race the user lands here. reload_to_origin()
|
||||||
|
restores the original URL (with UUID) — plain location.reload() would
|
||||||
|
reload the current SvelteKit path which may not have the UUID. -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm preset-tonal-primary border-primary-500 border"
|
||||||
|
onclick={reload_to_origin}>
|
||||||
|
<span class="fas fa-redo m-1"></span>
|
||||||
|
Reload / Retry
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => {
|
||||||
|
localStorage.removeItem('ae_idaa_loc');
|
||||||
|
db_posts.post.clear().catch(() => {});
|
||||||
|
db_posts.comment.clear().catch(() => {});
|
||||||
|
db_archives.archive.clear().catch(() => {});
|
||||||
|
db_archives.content.clear().catch(() => {});
|
||||||
|
db_events.event.clear().catch(() => {});
|
||||||
|
reload_to_origin();
|
||||||
|
}}
|
||||||
|
class="btn btn-sm preset-tonal-surface preset-outlined-warning-100-900 hover:preset-filled-warning-200-800 transition-all">
|
||||||
|
<span class="fas fa-sync-alt m-1"></span>
|
||||||
|
Clear Cache & Reload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $ae_loc.edit_mode}
|
||||||
|
<div class="mt-2 rounded border border-gray-300 bg-gray-50 p-2 text-left text-xs text-gray-500 dark:border-gray-700 dark:bg-gray-900">
|
||||||
|
<p class="font-mono">Debug (edit mode only)</p>
|
||||||
|
{#if $idaa_loc.novi_uuid}
|
||||||
|
<p>UUID: {$idaa_loc.novi_uuid}</p>
|
||||||
|
<p>Name: {$idaa_loc.novi_full_name ?? 'not set'}</p>
|
||||||
|
<p>Email: {$idaa_loc.novi_email ?? 'not set'}</p>
|
||||||
|
{:else}
|
||||||
|
<p>No Novi UUID in store</p>
|
||||||
|
{/if}
|
||||||
|
<p>iframe: {$ae_loc.iframe}</p>
|
||||||
|
<p>authenticated_access: {$ae_loc.authenticated_access}</p>
|
||||||
|
<p>trusted_access: {$ae_loc.trusted_access}</p>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user