fix(idaa): ensure breakout links preserve site access key and uuid
Proactively re-injects 'key' (site access key) and 'uuid' (Novi token) into 'Open Externally' and 'Copy Link' URLs on the Video Conferences page. This prevents authentication failures when members open meetings in a new browser tab after SvelteKit internal navigation has dropped the bootstrap parameters. Updated CLIENT__IDAA_and_customized_mods.md to document the requirement for these keys in breakout URLs.
This commit is contained in:
@@ -31,6 +31,17 @@ IDAA is a private membership organization for physicians in recovery. They use t
|
|||||||
|
|
||||||
IDAA's Aether instance is embedded as an **iframe inside their existing Novi-powered website** (`idaa.org`). Novi is their external Association Management System (AMS) — it handles membership records and authentication. Aether receives the member context via URL parameters on iframe load.
|
IDAA's Aether instance is embedded as an **iframe inside their existing Novi-powered website** (`idaa.org`). Novi is their external Association Management System (AMS) — it handles membership records and authentication. Aether receives the member context via URL parameters on iframe load.
|
||||||
|
|
||||||
|
### Breakout Links and Iframe Persistence
|
||||||
|
|
||||||
|
Members often need to open Jitsi meetings outside the Novi iframe (e.g., for full-screen features or on mobile). These are referred to as **Breakout Links**.
|
||||||
|
|
||||||
|
- **The Problem:** SvelteKit client-side navigation within the iframe often drops "bootstrap" query parameters like `?key=...` (site access key) and `?uuid=...` (Novi identity token).
|
||||||
|
- **The Requirement:** When a member breaks out of the iframe into a new browser tab, these keys **must** be present in the URL. Without them, the member will hit the site-domain gate or the IDAA auth gate and see "Access Denied."
|
||||||
|
- **The Solution:** The Video Conferences page uses a derived `breakout_url` that proactively re-injects the missing `key` (from `$ae_loc.allow_access`) and `uuid` (from `$idaa_loc.novi_uuid`) before generating the external link.
|
||||||
|
|
||||||
|
**Example Breakout URL:**
|
||||||
|
`https://client.oneskyit.com/idaa/video_conferences?uuid=...&key=...&room=...`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture: Composite Module
|
## Architecture: Composite Module
|
||||||
|
|||||||
@@ -57,17 +57,57 @@ let duration_timer_id: any = $state(null);
|
|||||||
let show_breakout_modal: boolean = $state(false);
|
let show_breakout_modal: boolean = $state(false);
|
||||||
let breakout_link_copied: boolean = $state(false);
|
let breakout_link_copied: boolean = $state(false);
|
||||||
|
|
||||||
|
const breakout_url = $derived.by(() => {
|
||||||
|
const url = new URL($page.url.href);
|
||||||
|
|
||||||
|
// Proactively inject site access key if missing or empty.
|
||||||
|
// WHY: SvelteKit internal navigation drops bootstrap params (key, uuid). When a member
|
||||||
|
// "breaks out" of the Novi iframe, they need these keys in the URL to satisfy the
|
||||||
|
// site-domain gate and the IDAA Novi auth gate in a fresh tab.
|
||||||
|
if (!url.searchParams.get('key')) {
|
||||||
|
const access_key = $ae_loc.allow_access;
|
||||||
|
if (typeof access_key === 'string' && access_key) {
|
||||||
|
url.searchParams.set('key', access_key);
|
||||||
|
} else if ($ae_loc.site_access_key) {
|
||||||
|
url.searchParams.set('key', $ae_loc.site_access_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proactively inject Novi UUID if missing or empty (ensures auth persistence).
|
||||||
|
if (!url.searchParams.get('uuid') && $idaa_loc.novi_uuid) {
|
||||||
|
url.searchParams.set('uuid', $idaa_loc.novi_uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.href;
|
||||||
|
});
|
||||||
|
|
||||||
function copy_meeting_link() {
|
function copy_meeting_link() {
|
||||||
navigator.clipboard.writeText($page.url.href).then(() => {
|
navigator.clipboard.writeText(breakout_url).then(() => {
|
||||||
breakout_link_copied = true;
|
breakout_link_copied = true;
|
||||||
setTimeout(() => (breakout_link_copied = false), 2000);
|
setTimeout(() => (breakout_link_copied = false), 2000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function build_jitsi_reports_href() {
|
function build_jitsi_reports_href() {
|
||||||
const url = new URL($page.url);
|
const url = new URL($page.url.href);
|
||||||
url.pathname = '/idaa/jitsi_reports';
|
url.pathname = '/idaa/jitsi_reports';
|
||||||
url.searchParams.delete('room');
|
url.searchParams.delete('room');
|
||||||
|
|
||||||
|
// Proactively inject site access key if missing or empty
|
||||||
|
if (!url.searchParams.get('key')) {
|
||||||
|
const access_key = $ae_loc.allow_access;
|
||||||
|
if (typeof access_key === 'string' && access_key) {
|
||||||
|
url.searchParams.set('key', access_key);
|
||||||
|
} else if ($ae_loc.site_access_key) {
|
||||||
|
url.searchParams.set('key', $ae_loc.site_access_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proactively inject Novi UUID if missing or empty
|
||||||
|
if (!url.searchParams.get('uuid') && $idaa_loc.novi_uuid) {
|
||||||
|
url.searchParams.set('uuid', $idaa_loc.novi_uuid);
|
||||||
|
}
|
||||||
|
|
||||||
return `${url.pathname}${url.search}${url.hash}`;
|
return `${url.pathname}${url.search}${url.hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1172,7 +1212,7 @@ async function init_jitsi() {
|
|||||||
mt-2 flex w-full flex-col flex-wrap items-center justify-center gap-1
|
mt-2 flex w-full flex-col flex-wrap items-center justify-center gap-1
|
||||||
border-t-2 border-dashed border-gray-400 pt-2 sm:flex-row">
|
border-t-2 border-dashed border-gray-400 pt-2 sm:flex-row">
|
||||||
<MyClipboard
|
<MyClipboard
|
||||||
value={encodeURI($page.url.href)}
|
value={encodeURI(breakout_url)}
|
||||||
btn_text="Copy Break-out Link"
|
btn_text="Copy Break-out Link"
|
||||||
btn_title="Copy a link to this meeting that can be opened outside of the Novi iframe."
|
btn_title="Copy a link to this meeting that can be opened outside of the Novi iframe."
|
||||||
btn_class="mt-2 px-2 py-1 bg-indigo-400 text-white rounded hover:bg-indigo-500"
|
btn_class="mt-2 px-2 py-1 bg-indigo-400 text-white rounded hover:bg-indigo-500"
|
||||||
@@ -1312,7 +1352,7 @@ async function init_jitsi() {
|
|||||||
{breakout_link_copied ? 'Copied!' : 'Copy Link'}
|
{breakout_link_copied ? 'Copied!' : 'Copy Link'}
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href={$page.url.href}
|
href={breakout_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
onclick={() => (show_breakout_modal = false)}
|
onclick={() => (show_breakout_modal = false)}
|
||||||
@@ -1327,7 +1367,7 @@ async function init_jitsi() {
|
|||||||
<textarea
|
<textarea
|
||||||
readonly
|
readonly
|
||||||
rows="3"
|
rows="3"
|
||||||
value={$page.url.href}
|
value={breakout_url}
|
||||||
onclick={(e) => (e.target as HTMLTextAreaElement).select()}
|
onclick={(e) => (e.target as HTMLTextAreaElement).select()}
|
||||||
class="w-full cursor-text resize-none rounded border border-gray-200 bg-gray-50 px-2 py-1.5 font-mono text-xs text-gray-700 focus:outline-none"
|
class="w-full cursor-text resize-none rounded border border-gray-200 bg-gray-50 px-2 py-1.5 font-mono text-xs text-gray-700 focus:outline-none"
|
||||||
title="Click to select all, then copy (ctrl/cmd + C)"
|
title="Click to select all, then copy (ctrl/cmd + C)"
|
||||||
|
|||||||
Reference in New Issue
Block a user