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:
@@ -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_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 `<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`, `h1–h6`) 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
|
||||
|
||||
Reference in New Issue
Block a user