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">
|
||||
import { untrack } from 'svelte';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
let log_lvl: number = 0;
|
||||
|
||||
// *** 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.
|
||||
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.
|
||||
//
|
||||
// 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.
|
||||
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);
|
||||
|
||||
$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(() => {
|
||||
@@ -211,14 +226,17 @@ $effect(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
// TTL cache: skip if this UUID was recently verified.
|
||||
// Prevents duplicate API calls when site_cfg_json updates multiple times (SWR pattern).
|
||||
// TTL cache: skip if this UUID was recently verified AND $ae_loc still has permissions.
|
||||
// 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();
|
||||
if (
|
||||
$idaa_loc.novi_verified &&
|
||||
$idaa_loc.novi_uuid === current_uuid &&
|
||||
$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}`);
|
||||
novi_verifying = false;
|
||||
@@ -445,7 +463,7 @@ function handle_verify_retry() {
|
||||
db_archives.archive.clear().catch(() => {});
|
||||
db_archives.content.clear().catch(() => {});
|
||||
db_events.event.clear().catch(() => {});
|
||||
location.reload();
|
||||
reload_to_origin();
|
||||
}}>
|
||||
<span class="fas fa-redo m-1"></span>
|
||||
Reset & Retry
|
||||
@@ -496,7 +514,7 @@ function handle_verify_retry() {
|
||||
db_archives.archive.clear().catch(() => {});
|
||||
db_archives.content.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">
|
||||
<span class="fas fa-sync-alt m-1"></span>
|
||||
@@ -513,6 +531,8 @@ function handle_verify_retry() {
|
||||
}
|
||||
localStorage.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();
|
||||
}}
|
||||
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} -->
|
||||
{@render children?.()}
|
||||
{#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 class="text-center text-sm text-gray-500">
|
||||
IDAA Novi UUID not found in URL param!
|
||||
<p class="text-center text-xs text-gray-500">
|
||||
Novi: {$idaa_loc.novi_uuid} · {$idaa_loc.novi_full_name ?? 'name not set'} · {$idaa_loc.novi_email ?? 'email not set'}
|
||||
</p>
|
||||
{/if}
|
||||
{: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
|
||||
class="container m-8 flex w-full flex-col items-center justify-center gap-1 p-8 font-bold">
|
||||
<h1>
|
||||
class="container m-8 flex w-full flex-col items-center justify-center gap-3 p-8 text-center">
|
||||
<h1 class="font-bold">
|
||||
<span class="text-red-500">
|
||||
<span class="fas fa-exclamation-triangle"></span>
|
||||
Access Denied
|
||||
<span class="fas fa-exclamation-triangle"></span>
|
||||
</span>
|
||||
</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}
|
||||
In iframe mode
|
||||
{/if}
|
||||
|
||||
{#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 class="text-xs italic text-gray-500">
|
||||
If you just opened this page, try reloading. If the problem persists, try "Clear Cache & Reload".
|
||||
</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}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user