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}