feat: auto-expand presenter auth to all roles at sign-in

When a presenter or session POC signs in via their access link, expand_auth_for_person
now queries both Dexie (warm cache) and the API in parallel so that cold-cache sessions
are covered. All presenter records and POC sessions for the same person_id in the event
are granted auth in one shot — a multi-session presenter no longer needs to click every
individual link.

Agreed button now also unlocks via person_id match (not only auth__kv.presenter[id]),
covering the case where a presenter signed in via a different session's link and wasn't
individually in auth__kv for this record yet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-15 16:51:22 -04:00
parent 01b1729a67
commit 28025df038
2 changed files with 61 additions and 29 deletions

View File

@@ -24,6 +24,17 @@ import AE_Record_Controls from '$lib/ae_elements/AE_Record_Controls.svelte';
let show_modal = $state(false); let show_modal = $state(false);
let show_help = $state(false); let show_help = $state(false);
// WHY: auth__kv.presenter[id] is only set when a sign-in link is clicked for that
// specific presenter record. A person presenting in multiple sessions would need to
// click every link. The person_id match covers them across all their presenter records
// once they've signed in via any one link.
let presenter_agree_enabled = $derived(
$ae_loc.trusted_access ||
!!events_auth_loc.current.auth__kv.presenter[$lq__event_presenter_obj?.event_presenter_id ?? ''] ||
(!!events_auth_loc.current.auth__person.id &&
events_auth_loc.current.auth__person.id === $lq__event_presenter_obj?.person_id)
);
async function on_toggle(field: string, new_val: boolean) { async function on_toggle(field: string, new_val: boolean) {
await api.update_ae_obj({ await api.update_ae_obj({
api_cfg: $ae_api, api_cfg: $ae_api,
@@ -100,10 +111,7 @@ async function on_delete(method: 'delete' | 'disable') {
{#if $lq__event_presenter_obj?.agree} {#if $lq__event_presenter_obj?.agree}
<button <button
type="button" type="button"
disabled={!$ae_loc.trusted_access && disabled={!presenter_agree_enabled}
!events_auth_loc.current.auth__kv.presenter[
$lq__event_presenter_obj?.event_presenter_id
]}
onclick={() => { onclick={() => {
$events_slct.event_presentation_id = $events_slct.event_presentation_id =
$lq__event_presenter_obj?.event_presentation_id_random; $lq__event_presenter_obj?.event_presentation_id_random;
@@ -120,10 +128,7 @@ async function on_delete(method: 'delete' | 'disable') {
{:else} {:else}
<button <button
type="button" type="button"
disabled={!$ae_loc.trusted_access && disabled={!presenter_agree_enabled}
!events_auth_loc.current.auth__kv.presenter[
$lq__event_presenter_obj?.event_presenter_id
]}
onclick={() => { onclick={() => {
$events_slct.event_presentation_id = $events_slct.event_presentation_id =
$lq__event_presenter_obj?.event_presentation_id_random; $lq__event_presenter_obj?.event_presentation_id_random;

View File

@@ -33,7 +33,8 @@ import {
events_trig_kv events_trig_kv
} from '$lib/stores/ae_events_stores'; } from '$lib/stores/ae_events_stores';
import { events_auth_loc } from '$lib/stores/ae_events_stores__auth.svelte'; import { events_auth_loc } from '$lib/stores/ae_events_stores__auth.svelte';
import { db_events } from '$lib/ae_events/db_events'; import { db_events, type Presenter, type Session } from '$lib/ae_events/db_events';
import { api } from '$lib/api/api';
import { LogIn, X } from '@lucide/svelte'; import { LogIn, X } from '@lucide/svelte';
onMount(() => { onMount(() => {
console.log('Browser environment detected.'); console.log('Browser environment detected.');
@@ -102,14 +103,12 @@ onMount(() => {
}); });
// Grants auth for every presenter record and POC session this person holds in the event. // Grants auth for every presenter record and POC session this person holds in the event.
// WHY: presenters and POCs can appear in multiple sessions/presentations. Clicking a single // WHY: a single sign-in link only grants auth for one presenter_id/session_id. This
// sign-in link only grants auth for that specific presenter_id/session_id. This scan // queries both Dexie (fast, warm cache) AND the API (reliable, cold cache) in parallel,
// pre-populates auth__kv for all their other roles so they don't have to click each link. // merges the results, and pre-populates auth__kv for all roles in one shot.
// Runs against Dexie cache — benign no-op if cache is cold; person_id matching in auth
// checks provides fallback coverage for navigation until cache warms.
async function expand_auth_for_person(person_id: string, event_id: string) { async function expand_auth_for_person(person_id: string, event_id: string) {
try { try {
const [presenter_li, session_li] = await Promise.all([ const [dexie_presenters, dexie_sessions, api_presenters, api_sessions] = await Promise.all([
db_events.presenter db_events.presenter
.where('person_id').equals(person_id) .where('person_id').equals(person_id)
.filter(p => p.event_id === event_id) .filter(p => p.event_id === event_id)
@@ -117,24 +116,52 @@ async function expand_auth_for_person(person_id: string, event_id: string) {
db_events.session db_events.session
.where('poc_person_id').equals(person_id) .where('poc_person_id').equals(person_id)
.filter(s => s.event_id === event_id) .filter(s => s.event_id === event_id)
.toArray() .toArray(),
api.search_ae_obj({
api_cfg: $ae_api,
obj_type: 'event_presenter',
search_query: { and: [
{ field: 'event_id', op: 'eq', value: event_id },
{ field: 'person_id', op: 'eq', value: person_id }
]},
limit: 200
}).catch(() => null),
api.search_ae_obj({
api_cfg: $ae_api,
obj_type: 'event_session',
search_query: { and: [
{ field: 'event_id', op: 'eq', value: event_id },
{ field: 'poc_person_id', op: 'eq', value: person_id }
]},
limit: 200
}).catch(() => null)
]); ]);
for (const p of presenter_li) {
if (p.event_presenter_id) { // Merge Dexie + API results, deduplicated by ID using plain objects (not Map)
events_auth_loc.current.auth__kv.presenter[p.event_presenter_id] = true; const api_presenter_li: Presenter[] = Array.isArray(api_presenters) ? (api_presenters as Presenter[]) : [];
events_auth_loc.current.auth__kv.presentation[p.event_presentation_id] = true; const api_session_li: Session[] = Array.isArray(api_sessions) ? (api_sessions as Session[]) : [];
// 'read' matches what presenter_sign_in sets — truthy, but signals read-only session access const presenter_kv: Record<string, Presenter> = {};
events_auth_loc.current.auth__kv.session[p.event_session_id] = 'read'; for (const p of [...dexie_presenters, ...api_presenter_li]) {
} if (p.event_presenter_id) presenter_kv[p.event_presenter_id] = p;
} }
for (const s of session_li) { const session_kv: Record<string, Session> = {};
if (s.event_session_id) { for (const s of [...dexie_sessions, ...api_session_li]) {
events_auth_loc.current.auth__kv.session[s.event_session_id] = true; if (s.event_session_id) session_kv[s.event_session_id] = s;
}
} }
console.log(`expand_auth_for_person: ${presenter_li.length} presenter(s), ${session_li.length} POC session(s) auto-granted`);
for (const p of Object.values(presenter_kv)) {
events_auth_loc.current.auth__kv.presenter[p.event_presenter_id] = true;
if (p.event_presentation_id) events_auth_loc.current.auth__kv.presentation[p.event_presentation_id] = true;
// 'read' matches presenter_sign_in — truthy, signals read-only session access for presenters
if (p.event_session_id) events_auth_loc.current.auth__kv.session[p.event_session_id] = 'read';
}
for (const s of Object.values(session_kv)) {
events_auth_loc.current.auth__kv.session[s.event_session_id] = true;
}
console.log(`expand_auth_for_person: ${Object.keys(presenter_kv).length} presenter(s), ${Object.keys(session_kv).length} POC session(s) auto-granted`);
} catch (e) { } catch (e) {
console.warn('expand_auth_for_person: Dexie lookup failed', e); console.warn('expand_auth_for_person: lookup failed', e);
} }
} }