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:
Scott Idem
2026-03-09 14:48:49 -04:00
parent 7df887fabd
commit eb0dcb17f8
8 changed files with 2538 additions and 513 deletions

View File

@@ -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/<id>` 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=<url-encoded-email>
&full_name=<url-encoded-name>
&iframe=true
&key=<site-access-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_13, 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_*` (SunSat 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 `<style>` block with `@apply`. Tailwind v4 requires
`@reference "../../../../app.css";` at the top of any component `<style>` block that uses `@apply`.
- The country subdivision lookup list (`lu_country_subdivision_list`) contains duplicate entries —
specifically Puerto Rico (`PR`) has two rows with `code = '-'`. The `{#each}` key must use
the array index (`i`) rather than `sub.code` to avoid a Svelte `each_key_duplicate` error.
The duplicate entries are a **backend data quality issue** that should be cleaned up in the DB.
### Demo / Test IDs
No dedicated IDAA recovery meeting demo records — uses the standard Event demo record for dev:
- Event: `pjrcghqwert` (id: 1) "Demo One Sky IT Conference"
@@ -241,9 +330,10 @@ Four stores manage all IDAA state:
Stores Novi auth context and per-submodule query settings:
```typescript
{
novi_uuid: string | null // Member UUID from Novi
novi_email: string | null
novi_full_name: string | null
novi_uuid: string | null // Member UUID (set on verification success)
novi_email: string | null // Verified email from Novi API
novi_full_name: string | null // Verified name from Novi API
novi_verified: boolean // true after successful Novi API verification
novi_admin_li: string[] // Admin UUID list (from site config)
novi_trusted_li: string[] // Trusted member UUID list
novi_jitsi_mod_li: string[] // Jitsi moderator UUIDs
@@ -288,6 +378,44 @@ The IDAA module is embedded in `idaa.org` via iframe. This requires:
2. **URL parameter auth** — Novi passes member context via query string on load
3. **No standard navigation** — Members navigate within the iframe; Aether's nav chrome is hidden or minimal in this context
### Novi UUID Verification Flow
**Iframe HTML files** (in `static/`): Pass only `uuid` to the iframe src — no Novi API calls in the browser:
```text
idaa_novi_iframe_archives.html
idaa_novi_iframe_bulletin_board.html
idaa_novi_iframe_recovery_meetings.html
idaa_novi_iframe_jitsi_meeting.html ← reference pattern (unchanged)
```
**SvelteKit layout** (`(idaa)/+layout.svelte`): Calls `GET /customers/{uuid}` on the Novi API using the `novi_idaa_api_key` from `site_cfg_json`. Sets verified name/email in `$idaa_loc` and grants permissions. Shows a "Verifying identity..." spinner during the async call.
**Jitsi page** (`video_conferences/+page.svelte`): Checks `$idaa_loc.novi_verified` in `fetch_novi_data()`. If the layout already verified the user, it reuses `$idaa_loc.novi_email` / `$idaa_loc.novi_full_name` and skips the duplicate member details API call. The group moderator check (`get_novi_group_moderators`) always runs — it is Jitsi-specific.
### ⚠️ Iframe CSS Conflicts (Bootstrap v3)
When `$ae_loc.iframe = true`, the root layout (`+layout.svelte`) injects two external stylesheets from Novi's CDN:
```text
https://assets-staging.noviams.com/novi-core-assets/css/fontawesome.css — safe, icon-only
https://assets-staging.noviams.com/novi-core-assets/css/c/idaa/idaa.css — Bootstrap v3.4.1 ⚠️
```
`idaa.css` is a full **Bootstrap v3.4.1** bundle. It applies global styles to bare HTML elements
(`input`, `select`, `textarea`, `h1h6`) and commonly named classes (`.btn`, `.badge`, `.active`,
`.text-*`, `.bg-*`). These will compete with Tailwind v4 + Skeleton UI.
**Known consequences:**
- Bare form elements (`<input>`, `<select>`) receive Bootstrap's height/padding resets on top of Tailwind
- `.btn` class gets Bootstrap button colors, potentially overriding `preset-*` Skeleton classes
- `<section>` and heading elements may get unexpected margins/padding from Bootstrap's typography reset
- Class names `.field-input` and `.field-label` (used in the v2 edit form's scoped `<style>` block)
also exist in `idaa.css`'s date picker — Svelte's scoped attribute selector wins, but be aware
**Mitigation:** The iframe CSS conflicts existed before v2 and are not new. The v2 form uses the
same Skeleton/Tailwind component classes as the rest of the app. Avoid using bare `<section>`,
`<article>`, or block-level HTML5 elements as style hooks; use `<div>` with explicit classes instead.
---
## Testing Requirements
@@ -348,5 +476,5 @@ ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
---
**Document Status:** ✅ Complete (initial)
**Last Verified:** 2026-02-26 — reverse-engineered from source code
**Document Status:** ✅ Current
**Last Verified:** 2026-03-09 — updated for Novi UUID verification upgrade