Files
OSIT-AE-App-Svelte/src/routes/idaa/(idaa)/+layout.svelte

290 lines
10 KiB
Svelte

<script lang="ts">
import { untrack } from 'svelte';
let log_lvl: number = 0;
// *** Import Svelte specific
import { browser } from '$app/environment';
// *** Import Aether specific variables and functions
import { ae_util } from '$lib/ae_utils/ae_utils';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import { idaa_loc, idaa_sess, idaa_slct } from '$lib/stores/ae_idaa_stores';
interface Props {
/** @type {import('./$types').LayoutData} */
data: any;
children?: import('svelte').Snippet;
}
let { data, children }: Props = $props();
// True while verification is in flight OR while waiting for site config to load.
// Pre-initialized to true if a UUID is present so there is no flash of "Access Denied"
// on first render before the effect has a chance to run.
let novi_verifying: boolean = $state(
typeof window !== 'undefined' &&
!!new URLSearchParams(window.location.search).get('uuid')
);
// Effect 1: Set URL origin and params (unchanged from original)
$effect(() => {
untrack(() => {
$ae_loc.url_origin = data.url.origin;
$ae_loc.params = data.params;
});
if (log_lvl > 1) {
console.log(`+layout.svelte data:`, data);
}
});
// Effect 2: Novi UUID verification
// Only fires when a uuid is present in the URL (i.e. the Novi iframe path).
// Non-Novi sign-in paths (User/Pass, shared passcode) will never have a uuid param,
// so this block won't run for them — their permissions are unaffected.
$effect(() => {
if (!browser) return;
const uuid = data.url.searchParams.get('uuid'); // tracked — re-runs if URL changes
// WHY tracked outside untrack: on first load the fast-path returns a stale Dexie
// cache, so site_cfg_json may be missing novi_idaa_api_key when this effect first
// runs. The background refresh in ae_core__site.ts pushes fresh cfg_json into
// $ae_loc after the API responds. Tracking here means this effect re-runs at that
// point and retries verification with the correct key — no manual reload needed.
const site_cfg_json = $ae_loc.site_cfg_json;
untrack(() => {
if (!uuid) {
// No UUID in URL — non-Novi path, nothing to do here.
$idaa_loc.novi_verified = false;
novi_verifying = false;
return;
}
// Already verified for this exact UUID — don't repeat the round-trip.
// This guard fires when site_cfg_json changes for reasons unrelated to Novi.
if ($idaa_loc.novi_verified && $idaa_loc.novi_uuid === uuid) {
novi_verifying = false;
return;
}
// Load admin/trusted lists from site config first — needed by verify function.
// Only override if site_cfg_json actually provides them; falling back to [] would
// silently overwrite the hardcoded defaults in ae_idaa_stores.ts.
if (site_cfg_json?.novi_admin_li?.length) {
$idaa_loc.novi_admin_li = site_cfg_json.novi_admin_li;
}
if (site_cfg_json?.novi_trusted_li?.length) {
$idaa_loc.novi_trusted_li = site_cfg_json.novi_trusted_li;
}
const novi_api_key = site_cfg_json?.novi_idaa_api_key ?? null;
const novi_api_root_url =
site_cfg_json?.novi_api_root_url ?? 'https://www.idaa.org/api';
// Fire-and-forget the async verification. After the first await, Svelte's
// reactive tracking no longer applies, so writes to stores are safe.
novi_verifying = true;
verify_novi_uuid(uuid, novi_api_key, novi_api_root_url);
});
});
/**
* Verifies a Novi UUID against the Novi API and sets permissions accordingly.
* "All or nothing" — if no API key is configured or the call fails, access is denied.
* Called from within untrack(), so store writes here will not trigger reactive loops.
*/
async function verify_novi_uuid(
uuid: string,
api_key: string | null,
api_root_url: string
) {
console.log(`IDAA Layout: Starting Novi UUID verification for ${uuid}...`);
if (!api_key) {
// No Novi API key in site config. All-or-nothing means no UUID-based access.
console.warn(
'IDAA Layout: Novi API key not configured. UUID-based access denied.'
);
$idaa_loc.novi_uuid = null;
$idaa_loc.novi_email = null;
$idaa_loc.novi_full_name = null;
$idaa_loc.novi_verified = false;
novi_verifying = false;
return;
}
try {
if (log_lvl > 1) {
console.log(`IDAA Layout: Verifying Novi UUID ${uuid} via API...`);
}
const headers = new Headers();
headers.append('Authorization', `Basic ${api_key}`);
const response = await fetch(`${api_root_url}/customers/${uuid}`, {
method: 'GET',
headers
});
if (!response.ok) {
throw new Error(
`Novi API returned ${response.status} for UUID ${uuid}`
);
}
const result = await response.json();
// Build display name: prefer "First L." format, fall back to full Name field.
const first_name = result?.FirstName ?? null;
const last_initial = result?.LastName
? `${result.LastName.charAt(0).toUpperCase()}.`
: '';
const verified_name =
first_name && last_initial
? `${first_name} ${last_initial}`
: (result?.Name ?? null);
// Normalize email — Novi occasionally includes spaces where + should be.
const verified_email = result?.Email
? result.Email.replace(/\s+/g, '+')
: null;
$idaa_loc.novi_uuid = uuid;
$idaa_loc.novi_email = verified_email;
$idaa_loc.novi_full_name = verified_name;
$idaa_loc.novi_verified = true;
console.log(
`IDAA Layout: Novi UUID verified. Name: ${verified_name}, Email: ${verified_email}`
);
// Determine permission level based on verified UUID.
// UUID confirmed real → at minimum authenticated. Check lists for higher levels.
let target_novi_level = 'authenticated';
if ($idaa_loc.novi_admin_li?.includes(uuid)) {
target_novi_level = 'administrator';
} else if ($idaa_loc.novi_trusted_li?.includes(uuid)) {
target_novi_level = 'trusted';
}
// PERMISSION UPGRADE STRATEGY: only apply if higher than current level.
// This prevents a global 'manager' from being downgraded by the IDAA layout.
const current_level = $ae_loc.access_type || 'anonymous';
if (
ae_util.compare_access_levels(target_novi_level, current_level) ===
1
) {
console.log(
`IDAA Layout: Upgrading access from ${current_level} to ${target_novi_level} (Novi verified)`
);
const perms = ae_util.process_permission_checks(target_novi_level);
$ae_loc = { ...$ae_loc, ...perms };
} else {
if (log_lvl > 1) {
console.log(
`IDAA Layout: Keeping current access ${current_level} (Novi level ${target_novi_level} is not an upgrade)`
);
}
}
// Reset BB query filters to safe defaults in case they were left in a non-default state.
$idaa_loc.bb.qry__hidden = 'not_hidden';
$idaa_loc.bb.qry__enabled = 'enabled';
} catch (error) {
// Verification failed — all-or-nothing means deny access.
console.error(
`IDAA Layout: Novi UUID verification failed for ${uuid}:`,
error
);
$idaa_loc.novi_uuid = null;
$idaa_loc.novi_email = null;
$idaa_loc.novi_full_name = null;
$idaa_loc.novi_verified = false;
} finally {
novi_verifying = false;
}
}
</script>
{#if !browser}
<!-- SSR / pre-hydration placeholder -->
<p class="text-center text-sm text-gray-500">
<span class="fas fa-spinner fa-spin"></span>
Loading...
</p>
{:else if novi_verifying}
<!-- Async Novi API call is in flight — show spinner to prevent Access Denied flash -->
<div
class="container m-8 flex w-full flex-col items-center justify-center gap-2 p-8">
<p class="text-center text-sm text-gray-500">
<span class="fas fa-spinner fa-spin"></span>
Verifying identity...
</p>
</div>
{:else if $ae_loc.trusted_access || ($ae_loc.authenticated_access && $idaa_loc.novi_uuid)}
{@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!
</p>
{/if}
{:else}
<div
class="container m-8 flex w-full flex-col items-center justify-center gap-1 p-8 font-bold">
<h1>
<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 these 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>
{/if}
</div>
{/if}