diff --git a/src/lib/app_components/e_app_access_type.svelte b/src/lib/app_components/e_app_access_type.svelte index aca597c2..8fa429b2 100644 --- a/src/lib/app_components/e_app_access_type.svelte +++ b/src/lib/app_components/e_app_access_type.svelte @@ -6,6 +6,7 @@ import { afterNavigate } from '$app/navigation'; // *** Import other supporting libraries // import { liveQuery } from "dexie"; import { + Loader, Lock, LockOpen, ShieldEllipsis, @@ -60,6 +61,17 @@ let checked_passcode: null | string = $state(null); // let trigger: null|string|boolean = null; let trigger: null | string | boolean = $state(null); +// ── Secure passcode auth (API-verified) ────────────────────────────────────── +// Set USE_API_PASSCODE_AUTH = true to activate the new API-verified path. +// When false, the original local comparison runs unchanged. +// When true, the component POSTs to /api/authenticate_passcode and stores the +// returned JWT. On network or unexpected API errors it falls back to the local +// method automatically so IDAA staff and other users are never left locked out. +const USE_API_PASSCODE_AUTH = true; + +let auth_pending: boolean = $state(false); +let auth_error: string | null = $state(null); + // const dispatch = createEventDispatcher(); // WARNING: There is a bug (I think) around here related to the entered_passcode not being cleared. There seems to be something different about how Svelte handles state in this component compared to the others. This might be related to the `$effect` or `$derived` usage. Maybe there are conflicting things trying to update the $ae_loc store at the same time. @@ -96,17 +108,21 @@ onDestroy(() => { // }); $effect(() => { - if ( - entered_passcode && - entered_passcode.length >= 5 && - entered_passcode != checked_passcode - ) { - checked_passcode = entered_passcode; - if (log_lvl) { - console.log(`entered_passcode=${entered_passcode}`); + if (!USE_API_PASSCODE_AUTH) { + // Local path: synchronous comparison, safe to auto-fire on each qualifying keystroke. + if (entered_passcode && entered_passcode.length >= 5 && entered_passcode != checked_passcode) { + checked_passcode = entered_passcode; + if (log_lvl) console.log(`entered_passcode=${entered_passcode}`); + handle_check_passcode_local(); } - handle_check_access_type_passcode(); + return; } + // API path: debounce — fire 600ms after the user stops typing. + // The $effect cleanup (returned fn) cancels the previous timer on each new keystroke. + // Enter key bypasses the debounce via the onkeydown handler on the input. + if (!entered_passcode || entered_passcode.length < 5) return; + const timer = setTimeout(() => handle_check_access_type_passcode(), 600); + return () => clearTimeout(timer); }); $effect(() => { @@ -163,10 +179,113 @@ $effect(() => { } }); +// Clear the API auth error as soon as the user changes their input after a rejection. +// Reset the local-path guard (checked_passcode) when input is fully cleared so the +// user can retype the same code after clearing the field. +$effect(() => { + if (entered_passcode) { + auth_error = null; + } else { + checked_passcode = null; + } +}); + +// Dispatcher — called from the Enter-key handler on the input (API path) or +// from the $effect auto-check (local path). Guards against concurrent API calls. function handle_check_access_type_passcode() { + if (USE_API_PASSCODE_AUTH) { + if (auth_pending || !entered_passcode || entered_passcode.length < 5) return; + handle_check_passcode_api(); // async, intentionally not awaited + } else { + handle_check_passcode_local(); + } +} + +// ── New: API-verified passcode auth ────────────────────────────────────────── +// POSTs to /api/authenticate_passcode. On success stores the returned JWT so +// the session has a TTL and passcodes never need to be cached client-side. +// Falls back to the local method on any network or unexpected API error to +// ensure IDAA staff and onsite users are never locked out. +async function handle_check_passcode_api() { + const site_id = $ae_loc?.site_id; + if (!site_id || site_id === 'ghost' || !entered_passcode) { + // No valid site context yet — fall back to local comparison. + handle_check_passcode_local(); + return; + } + + auth_pending = true; + auth_error = null; + + try { + const api_key = + ($ae_api?.headers?.['x-aether-api-key'] as string) ?? + ($ae_api?.api_secret_key as string) ?? + ''; + + const resp = await fetch(`${$ae_api.base_url}/v3/action/auth/authenticate_passcode`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-aether-api-key': api_key + }, + body: JSON.stringify({ site_id, passcode: entered_passcode }) + }); + + const json = await resp.json(); + + if (resp.ok && json?.data?.role) { + const role = json.data.role as string; + const jwt = json.data.jwt as string | null; + + $ae_loc.access_type = role as typeof $ae_loc.access_type; + if (jwt) $ae_loc.jwt = jwt; + window.localStorage.setItem('access_type', role); + + entered_passcode = ''; + show_passcode_input = false; + trigger = 'process_permission_check'; + + $ae_loc.app_cfg.show_element__menu = false; + $ae_loc.app_cfg.show_element__menu_btn = true; + + if (!$ae_loc.iframe && $ae_loc.authenticated_access) { + $ae_loc.app_cfg.show_element__access_type = true; + $ae_loc.app_cfg.show_element__cfg = true; + } else if ($ae_loc.iframe && $ae_loc.trusted_access) { + $ae_loc.app_cfg.show_element__access_type = true; + $ae_loc.app_cfg.show_element__cfg = true; + } else { + $ae_loc.app_cfg.show_element__access_type = true; + $ae_loc.app_cfg.show_element__cfg = false; + } + } else if (resp.status === 401) { + auth_error = 'Incorrect passcode.'; + } else if (resp.status === 404) { + // Site not found — configuration problem; fall back silently. + console.warn('passcode auth: site not found, falling back to local'); + handle_check_passcode_local(); + } else { + // Unexpected response — fall back so the user isn't locked out. + console.warn('passcode auth: unexpected status', resp.status, '— falling back to local'); + handle_check_passcode_local(); + } + } catch (_err) { + // Network/parse error — fall back to local comparison. + console.warn('passcode auth: network error, falling back to local'); + handle_check_passcode_local(); + } finally { + auth_pending = false; + } +} + +// ── Legacy: local passcode comparison (original method) ────────────────────── +// Kept intact so USE_API_PASSCODE_AUTH=false preserves the existing behaviour +// exactly, and so the API path can fall back here on errors. +function handle_check_passcode_local() { if (log_lvl > 1) { console.log( - `*** handle_check_access_type_passcode() *** passcode list:`, + `*** handle_check_passcode_local() *** passcode list:`, $ae_loc.site_access_code_kv ); } @@ -508,11 +627,19 @@ function handle_clear_access() { class:hidden={!show_passcode_input} type="text" placeholder="Passcode" + onkeydown={(e) => { if (e.key === 'Enter') handle_check_access_type_passcode(); }} autofocus={show_passcode_input} /> - + + {#if auth_pending} + + {/if} {/if} + + {#if auth_error} + {auth_error} + {/if}