290 lines
10 KiB
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}
|