feat(idaa): enforce mandatory Novi UUID linkage for member content

CRITICAL IDENTITY FIX:
Ensures all member-generated content (Meetings, Posts, Comments) is explicitly linked to the creator's Novi UUID via 'external_person_id' at the moment of creation.

Changes:
- Added 'external_person_id' to creation payloads in Recovery Meetings and BB Posts.
- Implemented 'identity scavenging' from localStorage in submit handlers to prevent race conditions where Svelte stores are briefly null.
- Refactored Post Comment edit component to robustly initialize and save creator identity.
- Added 'The Novi UUID Rule' to IDAA documentation to mandate this pattern for future development.
- Added Playwright test to verify creation linkage and fixed a version-mismatch bug in the test auth helper.

Note: Archives and Archive Content are excluded as they do not require member ownership.
This commit is contained in:
Scott Idem
2026-04-07 22:07:53 -04:00
parent ef45a0ca0f
commit f2765d6a5e
6 changed files with 135 additions and 13 deletions

View File

@@ -43,9 +43,13 @@ let prom_api__post_comment_obj: any = $state();
let post_comment_form = $state({
content: $idaa_slct.post_comment_obj?.content ?? '',
anonymous: $idaa_slct.post_comment_obj?.anonymous ?? false,
external_person_id: $idaa_slct.post_comment_obj?.external_person_id ?? '',
full_name: $idaa_slct.post_comment_obj?.full_name ?? '',
email: $idaa_slct.post_comment_obj?.email ?? '',
external_person_id:
$idaa_slct.post_comment_obj?.external_person_id ||
$idaa_loc.novi_uuid ||
'',
full_name:
$idaa_slct.post_comment_obj?.full_name || $idaa_loc.novi_full_name || '',
email: $idaa_slct.post_comment_obj?.email || $idaa_loc.novi_email || '',
enable: $idaa_slct.post_comment_obj?.enable ?? true,
hide: $idaa_slct.post_comment_obj?.hide ?? false,
priority: $idaa_slct.post_comment_obj?.priority ?? false,
@@ -94,10 +98,29 @@ async function handle_submit_form(event: any) {
// SVELTE 5: Map values from local form state to payload
post_comment_do['content'] = content_new_html;
post_comment_do['anonymous'] = post_comment_form.anonymous;
post_comment_do['external_person_id'] =
post_comment_form.external_person_id;
post_comment_do['full_name'] = post_comment_form.full_name;
post_comment_do['email'] = post_comment_form.email;
// Robust scavenging of identity from storage if form state is missing it
let novi_uuid = post_comment_form.external_person_id;
let novi_full_name = post_comment_form.full_name;
let novi_email = post_comment_form.email;
if (!novi_uuid && typeof localStorage !== 'undefined') {
try {
const ls_val = localStorage.getItem('ae_idaa_loc');
if (ls_val) {
const ls_json = JSON.parse(ls_val);
novi_uuid = ls_json.novi_uuid;
novi_full_name = ls_json.novi_full_name;
novi_email = ls_json.novi_email;
}
} catch (e) {
/* ignore */
}
}
post_comment_do['external_person_id'] = novi_uuid;
post_comment_do['full_name'] = novi_full_name;
post_comment_do['email'] = novi_email;
post_comment_do['notify'] = post_comment_di.notify ?? true; // Default to true if not found
post_comment_do['hide'] = post_comment_form.hide;

View File

@@ -891,7 +891,7 @@ $effect(() => {
name="external_person_id"
value={$idaa_slct.post_obj?.external_person_id
? $idaa_slct.post_obj?.external_person_id
: ''}
: ($idaa_loc.novi_uuid ?? '')}
readonly={!$ae_loc.administrator_access}
class="input preset-tonal-surface hover:preset-filled-surface-100-900 form-control w-96" />
{/if}
@@ -918,7 +918,7 @@ $effect(() => {
name="full_name"
value={$idaa_slct.post_obj?.full_name
? $idaa_slct?.post_obj.full_name
: $idaa_loc.novi_full_name}
: ($idaa_loc.novi_full_name ?? '')}
readonly={!$ae_loc.trusted_access}
class="input preset-tonal-surface hover:preset-filled-surface-100-900 form-control w-96" />
</label>

View File

@@ -183,11 +183,30 @@ let creating = $state(false); // true while create API call is in-flight
creating = true;
$idaa_slct.post_obj = {};
// Robust scavenging of identity from storage if store is null
let novi_uuid = $idaa_loc.novi_uuid;
let novi_full_name = $idaa_loc.novi_full_name;
let novi_email = $idaa_loc.novi_email;
if (!novi_uuid && typeof localStorage !== 'undefined') {
try {
const ls_val = localStorage.getItem('ae_idaa_loc');
if (ls_val) {
const ls_json = JSON.parse(ls_val);
novi_uuid = ls_json.novi_uuid;
novi_full_name = ls_json.novi_full_name;
novi_email = ls_json.novi_email;
}
} catch (e) {
/* ignore */
}
}
let data_kv = {
external_person_id: $idaa_loc.novi_uuid,
external_person_id: novi_uuid,
title: '',
full_name: $idaa_loc.novi_full_name,
email: $idaa_loc.novi_email,
full_name: novi_full_name,
email: novi_email,
enable: true
};
if (log_lvl) {

View File

@@ -446,8 +446,22 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
$idaa_slct.event_id = null;
$idaa_slct.event_obj = {};
let novi_uuid = $idaa_loc.novi_uuid;
if (!novi_uuid && typeof localStorage !== 'undefined') {
try {
const ls_val = localStorage.getItem('ae_idaa_loc');
if (ls_val) {
const ls_json = JSON.parse(ls_val);
novi_uuid = ls_json.novi_uuid;
}
} catch (e) {
// Ignore storage errors
}
}
let data_kv = {
name: 'Change NEW Recovery Meeting Name'
name: 'Change NEW Recovery Meeting Name',
external_person_id: novi_uuid
};
if (log_lvl) {
console.log(