diff --git a/documentation/CLIENT__IDAA_and_customized_mods.md b/documentation/CLIENT__IDAA_and_customized_mods.md index f08d5cfc..21fb72b7 100644 --- a/documentation/CLIENT__IDAA_and_customized_mods.md +++ b/documentation/CLIENT__IDAA_and_customized_mods.md @@ -3,7 +3,7 @@ **Client:** International Doctors in Alcoholics Anonymous (IDAA) **Module Path:** `src/routes/idaa/` **State Stores:** `src/lib/stores/ae_idaa_stores.ts` -**Last Updated:** 2026-02-26 +**Last Updated:** 2026-03-09 (Novi UUID verification upgrade) --- @@ -74,15 +74,31 @@ src/routes/idaa/ │ │ ├── ae_idaa_comp__post_obj_id_edit.svelte │ │ └── ae_idaa_comp__post_comment_obj_id_edit.svelte │ ├── recovery_meetings/ # Recovery Meetings (Events repurposed) +│ │ ├── +layout.ts # Layout loader (auth, stores) +│ │ ├── +layout.svelte # Layout wrapper │ │ ├── +page.svelte # Meeting list + search filters +│ │ ├── ae_idaa_comp__event_obj_li_wrapper.svelte # List container/modal host +│ │ ├── ae_idaa_comp__event_obj_li.svelte # Individual list item card +│ │ ├── ae_idaa_comp__event_obj_qry.svelte # Query/filter bar +│ │ ├── ae_idaa_comp__event_obj_id_view.svelte # Meeting detail (read-only) +│ │ ├── ae_idaa_comp__event_obj_id_edit.svelte # Meeting edit form (v1, legacy — do not touch) +│ │ ├── ae_idaa_comp__event_obj_id_edit_v2.svelte # Meeting edit form (v2, active) │ │ └── [event_id]/ -│ │ ├── +page.svelte # Meeting detail -│ │ ├── ae_idaa_comp__event_obj_id_view.svelte -│ │ └── ae_idaa_comp__event_obj_id_edit.svelte +│ │ ├── +page.svelte # Meeting detail page — renders view OR edit based on session flag +│ │ └── +page.ts │ └── video_conferences/ # Jitsi video conference integration └── jitsi_reports/ # External Jitsi reporting ``` +> **Note:** Recovery Meetings has **two UI entry points**: +> 1. **Modal pattern** (primary list flow) — list, view, and edit components live at `recovery_meetings/` +> level, toggled via `$idaa_sess.recovery_meetings` session flags (`show__modal_view`, `show__modal_edit`). +> 2. **Direct page** (`[event_id]/+page.svelte`) — navigating to `/idaa/recovery_meetings/` renders +> the same view/edit components gated by `$idaa_sess.recovery_meetings.edit__event_obj`. +> +> Both patterns use `ae_idaa_comp__event_obj_id_edit_v2.svelte`. The edit form clears **both** +> `show__modal_edit` and `edit__event_obj` on save/cancel so it works correctly from either entry point. + --- ## Authentication: Novi UUID System @@ -92,18 +108,46 @@ IDAA members do not log in through Aether — they log in through Novi (idaa.org ### URL Parameters (on iframe load) ``` ?uuid=<36-char-uuid> -&email= -&full_name= &iframe=true +&key= +``` + +> **Security note (2026-03-09):** The iframe HTML files previously also passed `email` and `full_name` +> via URL params. These were unverifiable claims that could be spoofed via URL. They have been removed. +> The SvelteKit layout now fetches verified identity directly from the Novi API. +> See "Iframe Integration" → "Novi UUID Verification Flow" below. + +### Verification Flow (`(idaa)/+layout.svelte`) + +When a `uuid` param is present in the URL, the layout performs an **async Novi API call** to verify: + +1. The UUID actually exists in Novi's system (prevents fake/crafted UUIDs) +2. Gets verified name and email directly from Novi — these can't be forged via URL +3. Sets `$idaa_loc.novi_uuid`, `$idaa_loc.novi_email`, `$idaa_loc.novi_full_name` +4. Sets `$idaa_loc.novi_verified = true` on success + +A `novi_verifying` UI state prevents the "Access Denied" screen from flashing during the API round-trip. + +**All or nothing:** If the Novi API key is not configured, or the verification call fails, access is denied. There is no URL-param fallback. + +**Required `site_cfg_json` fields:** +```json +{ + "novi_idaa_api_key": "Base64-encoded-key-from-Novi", + "novi_api_root_url": "https://www.idaa.org/api", // optional, this is the default + "novi_admin_li": ["uuid-1", "uuid-2"], + "novi_trusted_li": ["uuid-3", "uuid-4"], + "novi_idaa_group_guid_li": ["group-uuid"] // Jitsi moderators only +} ``` ### Permission Levels (Ascending) | Level | Condition | Access | |---|---|---| -| Anonymous | No UUID or unrecognized | No access | -| Authenticated | Valid UUID (36 chars) | View own content, limited actions | -| Trusted | UUID in `novi_trusted_li` | Full member access to all IDAA content | -| Administrator | UUID in `novi_admin_li` | Full access + edit/manage | +| Anonymous | No UUID, unrecognized UUID, or verification failure | No access | +| Authenticated | UUID verified against Novi API | View own content, limited actions | +| Trusted | Verified UUID in `novi_trusted_li` | Full member access to all IDAA content | +| Administrator | Verified UUID in `novi_admin_li` | Full access + edit/manage | `novi_trusted_li` and `novi_admin_li` are managed in Aether site config (not in Novi directly). @@ -116,9 +160,14 @@ IDAA members do not log in through Aether — they log in through Novi (idaa.org This ensures that OSIT staff with `super` or `manager` roles retain full access regardless of Novi UUID status. +### Non-Novi Sign-in Paths (unaffected) +- **User/Pass or Auth Link:** No `uuid` in URL → layout Novi block does not run +- **Shared Passcode:** No `uuid` in URL → layout Novi block does not run + ### Access Gate (`(idaa)/+layout.svelte`) The inner layout blocks ALL rendering if the user is not authorized: -- Anonymous → "Access Denied" error page +- `novi_verifying = true` → "Verifying identity..." spinner +- Verification failed or no UUID → "Access Denied" error page - Access check runs before any child routes render --- @@ -214,9 +263,49 @@ Members can filter meetings by: Search is debounced (250ms) and uses the standard Aether SWR pattern. +### Edit Form — Sections and Key Fields + +The edit form (`ae_idaa_comp__event_obj_id_edit.svelte`) is organized into these sections. +All fields map directly to the `ae_Event` object; none are IDAA-specific custom fields. + +| Section | Key Fields | +| --- | --- | +| **General Information** | `name` (required), `description` (TipTap rich text), `type` (IDAA / Caduceus / Family Recovery) | +| **How to Attend** | `physical` (bool), `virtual` (bool) toggles; conditionally shows: | +| → Physical | `location_address_json` (name, line_1–3, city, state, postal, country), `location_text` (TipTap) | +| → Virtual | Platform toggle: **Zoom** (`attend_url_code` meeting ID, `attend_url_passcode`, `attend_json.zoom.passcode_enc`, `attend_json.zoom.domain`, `attend_json.zoom.full_url`), **Jitsi** (`attend_json.jitsi.*`), **Other** (`attend_url`, `attend_url_passcode`, `attend_phone`, `attend_phone_passcode`) | +| → Both | `attend_text` (TipTap — additional attendance instructions) | +| **Schedule** | `recurring_pattern` (weekly/every other week/monthly/other), `weekday_*` (Sun–Sat booleans), `timezone`, `recurring_start_time`, `recurring_end_time`, `recurring_text` (optional TipTap, auto-generated with `*gen*` prefix if blank) | +| **Contacts** | `external_person_id` (Novi UUID link), `contact_li_json[0]` (Contact 1: name, email, phone_mobile, phone_home, phone_office — name/email locked to Novi user by default), `contact_li_json[1]` (Contact 2: same fields, optional) | +| **Admin Options** | `status`, `hide`, `priority`, `sort`, `group`, `enable`, `notes` (TipTap) — **trusted_access only** | + +**Rich text fields** all use `AE_Comp_Editor_TipTap` with separate `*_new_html` state variables +(not bound to `$idaa_slct.event_obj` directly) to track change state for the save-button logic. + +**Zoom URL auto-generation:** Triggered by `$idaa_trig = 'update_zoom_full_url'`. An `$effect` +reconstructs `attend_json.zoom.full_url` from domain + meeting_id + passcode_enc whenever +the Meeting ID, Passcode, Encrypted Passcode, or Domain fields change. + +**Recurring text auto-generation:** If `recurring_text` is blank or contains the `*gen*` prefix, +the submit handler generates a human-readable string (e.g., `*gen* weekly: Monday, Wednesday at 7:00 PM America/Chicago`). +Members can opt into a custom text via "Add More Details?" (admin/trusted only). + +**Contact 1 lock:** Contact 1 name and email default to the logged-in Novi member's identity +(`$idaa_loc.novi_full_name`, `$idaa_loc.novi_email`). They are `readonly` unless the user +explicitly unlocks them via confirm dialog (or has administrator access). + ### Jitsi Integration Some virtual meetings are hosted via Jitsi. Members with a Jitsi moderator UUID (`novi_jitsi_mod_li`) have elevated permissions in video sessions. +### Edit Form — Implementation Notes (v2) + +- The v2 edit form uses a ` diff --git a/src/routes/idaa/(idaa)/video_conferences/+page.svelte b/src/routes/idaa/(idaa)/video_conferences/+page.svelte index 5e39aa3f..da7765c5 100644 --- a/src/routes/idaa/(idaa)/video_conferences/+page.svelte +++ b/src/routes/idaa/(idaa)/video_conferences/+page.svelte @@ -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 ?? []; diff --git a/static/idaa_novi_iframe_archives.html b/static/idaa_novi_iframe_archives.html index f1e92550..94194d13 100644 --- a/static/idaa_novi_iframe_archives.html +++ b/static/idaa_novi_iframe_archives.html @@ -3,128 +3,122 @@ - Novi Test Page + IDAA Novi Archives iframe Example Template Page - + } - + const url = new URL(location); + + // Check if archive_id is defined in the message + if (event.data.archive_id !== undefined) { + console.log(`Got AE Archives ID: ${event.data.archive_id}`); + idaa_ae_slct_archive_id = event.data.archive_id; + + if (event.data.archive_id) { + url.searchParams.set('archive_id', event.data.archive_id); + } else { + url.searchParams.delete('archive_id'); + } + history.pushState({}, '', url); + } + + // Check if archive_content_id is defined in the message + if (event.data.archive_content_id !== undefined) { + console.log(`Got AE Archives Content ID: ${event.data.archive_content_id}`); + idaa_ae_slct_archive_content_id = event.data.archive_content_id; + + if (event.data.archive_content_id) { + url.searchParams.set( + 'archive_content_id', + event.data.archive_content_id + ); + } else { + url.searchParams.delete('archive_content_id'); + } + history.pushState({}, '', url); + } + } else { + console.log(`No data in message? ${event}`); + } + }); + + - -

- -

diff --git a/static/idaa_novi_iframe_bulletin_board.html b/static/idaa_novi_iframe_bulletin_board.html index 31d5d9a2..7287e8b6 100644 --- a/static/idaa_novi_iframe_bulletin_board.html +++ b/static/idaa_novi_iframe_bulletin_board.html @@ -3,141 +3,118 @@ - Novi Test Page + IDAA Novi Bulletin Board iframe Example Template Page - + } + + const url = new URL(location); + + // Check if post_id is defined in the message + if (event.data.post_id !== undefined) { + console.log(`Got AE Post ID: ${event.data.post_id}`); + idaa_ae_slct_post_id = event.data.post_id; + + if (event.data.post_id) { + url.searchParams.set('post_id', event.data.post_id); + } else { + url.searchParams.delete('post_id'); + } + history.pushState({}, '', url); + } + + // Check if post_comment_id is defined in the message + if (event.data.post_comment_id !== undefined) { + console.log(`Got AE Post Comment ID: ${event.data.post_comment_id}`); + idaa_ae_slct_post_comment_id = event.data.post_comment_id; + + if (event.data.post_comment_id) { + url.searchParams.set('post_comment_id', event.data.post_comment_id); + } else { + url.searchParams.delete('post_comment_id'); + } + history.pushState({}, '', url); + } + } else { + console.log(`No data in message? ${event}`); + } + }); + + - -

- -

diff --git a/static/idaa_novi_iframe_recovery_meetings.html b/static/idaa_novi_iframe_recovery_meetings.html index 34a4723b..0d0fdd8f 100644 --- a/static/idaa_novi_iframe_recovery_meetings.html +++ b/static/idaa_novi_iframe_recovery_meetings.html @@ -3,152 +3,111 @@ - Novi Test Page + IDAA Novi Recovery Meetings iframe Example Template Page - + } + + const url = new URL(location); + + // Check if event_id is defined in the message + if (event.data.event_id !== undefined) { + console.log(`Got AE Events ID: ${event.data.event_id}`); + idaa_ae_slct_event_id = event.data.event_id; + + if (event.data.event_id) { + url.searchParams.set('event_id', event.data.event_id); + } else { + url.searchParams.delete('event_id'); + } + history.pushState({}, '', url); + } + } else { + console.log(`No data in message? ${event}`); + } + }); + + - -

- -