fix(idaa): upgrade Novi UUID verification to server-side API call
Previously, IDAA iframe access relied on trusting URL params (uuid, email, full_name) passed from Novi — any 36-char string granted authenticated access with no actual verification. The (idaa)/+layout.svelte now performs an async Novi API call on every UUID load to verify the UUID exists, fetches name/email directly from Novi (cannot be spoofed via URL), and sets $idaa_loc.novi_verified on success. All-or-nothing: if novi_idaa_api_key is absent or the call fails, access denied. - ae_idaa_stores.ts: add novi_verified boolean field to idaa_loc - (idaa)/+layout.svelte: async UUID verification with spinner to prevent Access Denied flash; permission upgrade-only strategy preserved - video_conferences/+page.svelte: skip duplicate Novi member details call if layout already verified ($idaa_loc.novi_verified check) - iframe HTML files: remove browser-side Novi API fetch and email/full_name params; pass only uuid; add README/START/STOP/WARNING comments for client staff; fix iframe-before-script DOM ordering bug - documentation: CLIENT__IDAA_and_customized_mods.md updated with full verification flow, site_cfg_json fields, permission table, access gate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,6 @@
|
||||
// *** Import Svelte specific
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// *** Import other supporting libraries
|
||||
|
||||
// *** Import Aether specific variables and functions
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import {
|
||||
@@ -20,8 +18,6 @@
|
||||
} from '$lib/stores/ae_stores';
|
||||
import { idaa_loc, idaa_sess, idaa_slct } from '$lib/stores/ae_idaa_stores';
|
||||
|
||||
// import Help_tech from '$lib/e_app_help_tech.svelte';
|
||||
|
||||
interface Props {
|
||||
/** @type {import('./$types').LayoutData} */
|
||||
data: any;
|
||||
@@ -30,6 +26,11 @@
|
||||
|
||||
let { data, children }: Props = $props();
|
||||
|
||||
// True while the async Novi API verification call is in flight.
|
||||
// Prevents the access gate from flashing "Access Denied" during the network round-trip.
|
||||
let novi_verifying: boolean = $state(false);
|
||||
|
||||
// Effect 1: Set URL origin and params (unchanged from original)
|
||||
$effect(() => {
|
||||
untrack(() => {
|
||||
$ae_loc.url_origin = data.url.origin;
|
||||
@@ -41,143 +42,164 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 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) {
|
||||
untrack(() => {
|
||||
if (data.url.searchParams.get('uuid')) {
|
||||
$idaa_loc.novi_uuid = data.url.searchParams.get('uuid'); // data.params.uuid;
|
||||
}
|
||||
if (data.url.searchParams.get('email')) {
|
||||
$idaa_loc.novi_email = decodeURIComponent(data.url.searchParams.get('email'));
|
||||
} else {
|
||||
$idaa_loc.novi_email = null;
|
||||
}
|
||||
if (data.url.searchParams.get('full_name')) {
|
||||
$idaa_loc.novi_full_name = decodeURIComponent(data.url.searchParams.get('full_name'));
|
||||
} else {
|
||||
$idaa_loc.novi_full_name = null;
|
||||
}
|
||||
// Only override lists from site_cfg_json if it actually provides them.
|
||||
// Falling back to [] would overwrite the hardcoded defaults in ae_idaa_stores.ts
|
||||
// and cause staff UUIDs to be silently ignored.
|
||||
if ($ae_loc.site_cfg_json?.novi_admin_li?.length) {
|
||||
$idaa_loc.novi_admin_li = $ae_loc.site_cfg_json.novi_admin_li;
|
||||
}
|
||||
if ($ae_loc.site_cfg_json?.novi_trusted_li?.length) {
|
||||
$idaa_loc.novi_trusted_li = $ae_loc.site_cfg_json.novi_trusted_li;
|
||||
}
|
||||
if (!browser) return;
|
||||
|
||||
// Determine target Novi-based access level
|
||||
let target_novi_level = 'anonymous';
|
||||
if ($idaa_loc.novi_uuid) {
|
||||
if ($idaa_loc.novi_admin_li?.includes($idaa_loc.novi_uuid)) {
|
||||
target_novi_level = 'administrator';
|
||||
} else if ($idaa_loc.novi_trusted_li?.includes($idaa_loc.novi_uuid)) {
|
||||
target_novi_level = 'trusted';
|
||||
} else if ($idaa_loc?.novi_uuid?.length == 36) {
|
||||
// Any valid Novi UUID (36 chars) grants authenticated access.
|
||||
// We do NOT require $ae_loc.iframe here — that flag may not be set
|
||||
// yet due to effect ordering, and having a UUID in the URL is
|
||||
// sufficient proof of a Novi-originated request.
|
||||
target_novi_level = 'authenticated';
|
||||
}
|
||||
}
|
||||
const uuid = data.url.searchParams.get('uuid'); // tracked — re-runs if URL changes
|
||||
|
||||
// PERMISSION UPGRADE STRATEGY:
|
||||
// Only apply Novi-based permissions if they are HIGHER than the current level.
|
||||
// This prevents a global 'manager' from being downgraded to 'administrator' or 'authenticated' 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 detected)`);
|
||||
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)`);
|
||||
}
|
||||
untrack(() => {
|
||||
if (!uuid) {
|
||||
// No UUID in URL — non-Novi path, nothing to do here.
|
||||
$idaa_loc.novi_verified = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Resetting these just in case...
|
||||
$idaa_loc.bb.qry__hidden = 'not_hidden';
|
||||
$idaa_loc.bb.qry__enabled = 'enabled';
|
||||
});
|
||||
}
|
||||
// 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 ($ae_loc.site_cfg_json?.novi_admin_li?.length) {
|
||||
$idaa_loc.novi_admin_li = $ae_loc.site_cfg_json.novi_admin_li;
|
||||
}
|
||||
if ($ae_loc.site_cfg_json?.novi_trusted_li?.length) {
|
||||
$idaa_loc.novi_trusted_li = $ae_loc.site_cfg_json.novi_trusted_li;
|
||||
}
|
||||
|
||||
const novi_api_key = $ae_loc.site_cfg_json?.novi_idaa_api_key ?? null;
|
||||
const novi_api_root_url =
|
||||
$ae_loc.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);
|
||||
});
|
||||
});
|
||||
|
||||
// let iframe = data.url.searchParams.get('iframe');
|
||||
// if (browser && iframe == 'true') {
|
||||
// console.log('Use iframe layout!');
|
||||
// // data_struct['iframe'] = iframe;
|
||||
// $ae_loc.iframe = true;
|
||||
/**
|
||||
* 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
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
// document.getElementsByTagName('html')[0].classList.add('iframe');
|
||||
// // document.getElementsByTagName('html')[0].classList.remove('dark');
|
||||
// // document.getElementsByTagName('html')[0].classList.remove('light');
|
||||
try {
|
||||
if (log_lvl > 1) {
|
||||
console.log(`IDAA Layout: Verifying Novi UUID ${uuid} via API...`);
|
||||
}
|
||||
|
||||
// // $ae_loc.app_cfg.show_element__access_type = false;
|
||||
// // $ae_loc.app_cfg.show_element__cfg = false;
|
||||
// } else if (browser && iframe == 'false') {
|
||||
// // data_struct['iframe'] = false;
|
||||
// $ae_loc.iframe = false;
|
||||
const headers = new Headers();
|
||||
headers.append('Authorization', `Basic ${api_key}`);
|
||||
const response = await fetch(`${api_root_url}/customers/${uuid}`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
// document.getElementsByTagName('html')[0].classList.remove('iframe');
|
||||
// // document.getElementsByTagName('html')[0].classList.add('light');
|
||||
// }
|
||||
if (!response.ok) {
|
||||
throw new Error(`Novi API returned ${response.status} for UUID ${uuid}`);
|
||||
}
|
||||
|
||||
// $effect(() => {
|
||||
// if ($ae_loc.iframe && $ae_loc.iframe_height && $ae_loc.iframe_height_modal_body) {
|
||||
// if (log_lvl > 1) {
|
||||
// console.log('Getting new dimensions for iframe with modal:', $ae_loc.iframe_height, $ae_loc.iframe_height_modal_body);
|
||||
// }
|
||||
const result = await response.json();
|
||||
|
||||
// let iframe_height = 0;
|
||||
// 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);
|
||||
|
||||
// if ($ae_loc.iframe_height > $ae_loc.iframe_height_modal_body) {
|
||||
// iframe_height = $ae_loc.iframe_height;
|
||||
// } else {
|
||||
// iframe_height = $ae_loc.iframe_height_modal_body;
|
||||
// Normalize email — Novi occasionally includes spaces where + should be.
|
||||
const verified_email = result?.Email
|
||||
? result.Email.replace(/\s+/g, '+')
|
||||
: null;
|
||||
|
||||
// // console.log($ae_loc.modal_dimensions);
|
||||
$idaa_loc.novi_uuid = uuid;
|
||||
$idaa_loc.novi_email = verified_email;
|
||||
$idaa_loc.novi_full_name = verified_name;
|
||||
$idaa_loc.novi_verified = true;
|
||||
|
||||
// if ($ae_loc.modal_dimensions && $ae_loc.modal_dimensions.header_height) {
|
||||
// iframe_height = iframe_height + $ae_loc.modal_dimensions.header_height;
|
||||
// }
|
||||
// if ($ae_loc.modal_dimensions && $ae_loc.modal_dimensions.footer_height) {
|
||||
// iframe_height = iframe_height + $ae_loc.modal_dimensions.footer_height;
|
||||
// }
|
||||
// // iframe_height = iframe_height + 50; // Just in case
|
||||
// }
|
||||
console.log(
|
||||
`IDAA Layout: Novi UUID verified. Name: ${verified_name}, Email: ${verified_email}`
|
||||
);
|
||||
|
||||
// if (log_lvl > 1) {
|
||||
// console.log(`Suggested new iframe_height with modal: ${iframe_height}`);
|
||||
// }
|
||||
// window.parent.postMessage({'iframe_height': iframe_height}, "*"); // This should be in pixels
|
||||
// } else if ($ae_loc.iframe && $ae_loc.iframe_height) {
|
||||
// if (log_lvl > 1) {
|
||||
// console.log('Suggested new iframe_height:', $ae_loc.iframe_height);
|
||||
// }
|
||||
// window.parent.postMessage({'iframe_height': $ae_loc.iframe_height}, "*"); // This should be in pixels
|
||||
// }
|
||||
// });
|
||||
// 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 && ($ae_loc.trusted_access || ($ae_loc.authenticated_access && $idaa_loc.novi_uuid))}
|
||||
<!-- e_class="w-xl" -->
|
||||
<!-- e_class="float-right" -->
|
||||
<!-- <Help_tech
|
||||
e_class="w-full"
|
||||
show_btn_class="absolute top-0 right-0"
|
||||
btn_class="novi_btn"
|
||||
additional_kv={{
|
||||
'novi_uuid': $idaa_loc.novi_uuid,
|
||||
'novi_email': $idaa_loc.novi_email,
|
||||
'novi_full_name': $idaa_loc.novi_full_name,
|
||||
}}
|
||||
>
|
||||
</Help_tech> -->
|
||||
<!-- <div
|
||||
bind:clientHeight={$ae_loc.iframe_height}
|
||||
> -->
|
||||
{#if !browser}
|
||||
<!-- SSR / pre-hydration placeholder -->
|
||||
<p class="text-sm text-gray-500 text-center">
|
||||
<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 flex flex-col gap-2 w-full items-center justify-center p-8 m-8">
|
||||
<p class="text-sm text-gray-500 text-center">
|
||||
<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?.()}
|
||||
<!-- </div> -->
|
||||
{#if $idaa_loc.novi_uuid}
|
||||
<span class="text-sm text-gray-500">
|
||||
Novi: <span class="fas fa-user m-1"></span>
|
||||
@@ -188,7 +210,7 @@
|
||||
{:else}
|
||||
<p class="text-sm text-gray-500 text-center">IDAA Novi UUID not found!</p>
|
||||
{/if}
|
||||
{:else if browser}
|
||||
{:else}
|
||||
<div class="container flex flex-col gap-1 w-full items-center justify-center font-bold p-8 m-8">
|
||||
<h1>
|
||||
<span class="text-red-500">
|
||||
@@ -214,9 +236,4 @@
|
||||
<p>IDAA Novi UUID not found!</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-gray-500 text-center">
|
||||
<span class="fas fa-spinner fa-spin"></span>
|
||||
Loading...
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
|
||||
import { idaa_loc } from '$lib/stores/ae_idaa_stores';
|
||||
import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
|
||||
import {
|
||||
create_ae_obj__activity_log,
|
||||
@@ -350,14 +351,23 @@
|
||||
const novi_api_key = $ae_loc.site_cfg_json?.novi_idaa_api_key;
|
||||
|
||||
if (novi_api_root_url && novi_api_key) {
|
||||
const member_details = await get_novi_member_details(user_id, novi_api_root_url, novi_api_key);
|
||||
if (member_details.display_name) {
|
||||
display_name = member_details.display_name;
|
||||
console.log(`Jitsi: Updated display_name from Novi: ${display_name}`);
|
||||
}
|
||||
if (member_details.email) {
|
||||
email = member_details.email;
|
||||
console.log(`Jitsi: Updated email from Novi: ${email}`);
|
||||
// If the IDAA layout already verified this UUID, re-use those results rather than
|
||||
// making a duplicate Novi API call. The layout runs on every IDAA route, so by the
|
||||
// time onMount fires here it will usually have completed verification already.
|
||||
if ($idaa_loc.novi_verified && $idaa_loc.novi_email) {
|
||||
display_name = $idaa_loc.novi_full_name ?? display_name;
|
||||
email = $idaa_loc.novi_email ?? email;
|
||||
console.log(`Jitsi: Using layout-verified Novi data for user ${user_id}`);
|
||||
} else {
|
||||
const member_details = await get_novi_member_details(user_id, novi_api_root_url, novi_api_key);
|
||||
if (member_details.display_name) {
|
||||
display_name = member_details.display_name;
|
||||
console.log(`Jitsi: Updated display_name from Novi: ${display_name}`);
|
||||
}
|
||||
if (member_details.email) {
|
||||
email = member_details.email;
|
||||
console.log(`Jitsi: Updated email from Novi: ${email}`);
|
||||
}
|
||||
}
|
||||
|
||||
const novi_idaa_group_guid_li = $ae_loc.site_cfg_json?.novi_idaa_group_guid_li ?? [];
|
||||
|
||||
Reference in New Issue
Block a user