From f62bd9fb7987d1336338f26872d28b88ad909acd Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Fri, 13 Feb 2026 19:10:32 -0500 Subject: [PATCH] security(api): harden V3 authentication and unify CRUD endpoint patterns Implemented critical security and architectural fixes to align the frontend with the Aether API V3 standard and resolve 403 Forbidden race conditions. - Unified CRUD Helpers: Updated get, create, update, and delete helpers to use the standard /v3/crud/{obj_type}/{id} paths, ensuring correct backend isolation context. - Auth Scavenging: Implemented direct localStorage scavenging for 'x-account-id' in core fetch helpers to prevent hydration race conditions in Svelte 5. - Header Cleanup: Purged redundant 'x-aether-api-token' and fixed misplaced protocol headers in global stores. - Reliability: Fixed 'Content-Type' typos and standardized kebab-case header normalization. --- src/lib/ae_api/api_get__crud_obj_id.ts | 46 +---- src/lib/ae_api/api_get_object.ts | 21 +- src/lib/ae_api/api_patch_object.ts | 21 +- src/lib/ae_api/api_post_object.ts | 21 +- src/lib/ae_core/ae_core__site.ts | 1 - src/lib/api/api.ts | 266 +------------------------ src/lib/stores/ae_stores.ts | 5 +- 7 files changed, 68 insertions(+), 313 deletions(-) diff --git a/src/lib/ae_api/api_get__crud_obj_id.ts b/src/lib/ae_api/api_get__crud_obj_id.ts index fd3c0dfe..49181687 100644 --- a/src/lib/ae_api/api_get__crud_obj_id.ts +++ b/src/lib/ae_api/api_get__crud_obj_id.ts @@ -47,50 +47,8 @@ export async function get_ae_obj_id_crud({ console.log(`*** get_ae_obj_id_crud() *** Type: ${obj_type} ID: ${obj_id}`); } - let endpoint = ''; - // Map object types to their respective CRUD endpoints - const objTypeToEndpointMap: Record = { - 'account': '/crud/account', - 'address': '/crud/address', - 'archive': '/crud/archive', - 'archive_content': '/crud/archive/content', - 'contact': '/crud/contact', - 'data_store': '/crud/data_store', - 'event': '/crud/event', - 'event_abstract': '/crud/event/abstract', - 'event_badge': '/crud/event/badge', - 'event_device': '/crud/event/device', - 'event_exhibit': '/crud/event/exhibit', - 'event_exhibit_tracking': '/crud/event/exhibit/tracking', - 'event_file': '/crud/event/file', - 'event_location': '/crud/event/location', - 'event_person': '/crud/event/person', - 'event_presentation': '/crud/event/presentation', - 'event_presenter': '/crud/event/presenter', - 'event_session': '/crud/event/session', - 'event_track': '/crud/event/track', - 'grant': '/crud/grant', - 'hosted_file': '/crud/hosted_file', - 'journal': '/crud/journal', - 'journal_entry': '/crud/journal/entry', - 'order': '/crud/order', - 'order_line': '/crud/order/line', - 'page': '/crud/page', - 'person': '/crud/person', - 'post': '/crud/post', - 'post_comment': '/crud/post/comment', - 'site': '/crud/site', - 'site_domain': '/crud/site/domain', - 'sponsorship_cfg': '/crud/sponsorship/cfg', - 'sponsorship': '/crud/sponsorship' - }; - - if (objTypeToEndpointMap[obj_type]) { - endpoint = `${objTypeToEndpointMap[obj_type]}/${obj_id}`; - } else { - console.error(`Unknown object type: ${obj_type}`); - return false; - } + // V3 Standard: Unified endpoint for all objects + const endpoint = `/v3/crud/${obj_type}/${obj_id}`; if (log_lvl > 1) { console.log('Endpoint:', endpoint); diff --git a/src/lib/ae_api/api_get_object.ts b/src/lib/ae_api/api_get_object.ts index 7c925f74..e8f96e52 100644 --- a/src/lib/ae_api/api_get_object.ts +++ b/src/lib/ae_api/api_get_object.ts @@ -72,8 +72,25 @@ export const get_object = async function get_object({ const merged_headers = { ...api_cfg['headers'], ...headers }; // Auto-promote account_id from api_cfg to header if missing - if (!merged_headers['x-account-id'] && api_cfg['account_id']) { - merged_headers['x-account-id'] = api_cfg['account_id']; + let account_id = merged_headers['x-account-id'] || api_cfg['account_id']; + + // IMMEDIATE ACCOUNT ID SCAVENGING: Read from localStorage to avoid race conditions + if (!account_id && typeof localStorage !== 'undefined') { + try { + const ae_loc_raw = localStorage.getItem('ae_loc'); + if (ae_loc_raw) { + const ae_loc_json = JSON.parse(ae_loc_raw); + if (ae_loc_json.account_id) { + account_id = ae_loc_json.account_id; + } + } + } catch (e) { + // Silently fail on storage read + } + } + + if (account_id) { + merged_headers['x-account-id'] = account_id; } // Handle "Bootstrap Paradox" for unauthenticated requests diff --git a/src/lib/ae_api/api_patch_object.ts b/src/lib/ae_api/api_patch_object.ts index 3b80775c..c3b5ed07 100644 --- a/src/lib/ae_api/api_patch_object.ts +++ b/src/lib/ae_api/api_patch_object.ts @@ -51,8 +51,25 @@ export const patch_object = async function patch_object({ const merged_headers = { ...api_cfg['headers'], ...headers }; // Auto-promote account_id from api_cfg to header if missing - if (!merged_headers['x-account-id'] && api_cfg['account_id']) { - merged_headers['x-account-id'] = api_cfg['account_id']; + let account_id = merged_headers['x-account-id'] || api_cfg['account_id']; + + // IMMEDIATE ACCOUNT ID SCAVENGING: Read from localStorage to avoid race conditions + if (!account_id && typeof localStorage !== 'undefined') { + try { + const ae_loc_raw = localStorage.getItem('ae_loc'); + if (ae_loc_raw) { + const ae_loc_json = JSON.parse(ae_loc_raw); + if (ae_loc_json.account_id) { + account_id = ae_loc_json.account_id; + } + } + } catch (e) { + // Silently fail on storage read + } + } + + if (account_id) { + merged_headers['x-account-id'] = account_id; } // Handle "Bootstrap Paradox" for unauthenticated requests diff --git a/src/lib/ae_api/api_post_object.ts b/src/lib/ae_api/api_post_object.ts index e9bbaccd..1a829c7b 100644 --- a/src/lib/ae_api/api_post_object.ts +++ b/src/lib/ae_api/api_post_object.ts @@ -71,8 +71,25 @@ export const post_object = async function post_object({ const merged_headers = { ...api_cfg['headers'], ...headers }; // Auto-promote account_id from api_cfg to header if missing - if (!merged_headers['x-account-id'] && api_cfg['account_id']) { - merged_headers['x-account-id'] = api_cfg['account_id']; + let account_id = merged_headers['x-account-id'] || api_cfg['account_id']; + + // IMMEDIATE ACCOUNT ID SCAVENGING: Read from localStorage to avoid race conditions + if (!account_id && typeof localStorage !== 'undefined') { + try { + const ae_loc_raw = localStorage.getItem('ae_loc'); + if (ae_loc_raw) { + const ae_loc_json = JSON.parse(ae_loc_raw); + if (ae_loc_json.account_id) { + account_id = ae_loc_json.account_id; + } + } + } catch (e) { + // Silently fail on storage read + } + } + + if (account_id) { + merged_headers['x-account-id'] = account_id; } // Handle "Bootstrap Paradox" for unauthenticated requests diff --git a/src/lib/ae_core/ae_core__site.ts b/src/lib/ae_core/ae_core__site.ts index afb653a8..4a3ac1aa 100644 --- a/src/lib/ae_core/ae_core__site.ts +++ b/src/lib/ae_core/ae_core__site.ts @@ -137,7 +137,6 @@ async function _refresh_site_domain_v3_background({ api_cfg, fqdn, view, log_lvl const auth_props = [ 'x-account-id', - 'x-aether-api-token', 'Authorization', 'authorization', 'jwt', diff --git a/src/lib/api/api.ts b/src/lib/api/api.ts index 9bf1634d..5103b4c3 100644 --- a/src/lib/api/api.ts +++ b/src/lib/api/api.ts @@ -125,88 +125,10 @@ export const create_ae_obj_crud = async function create_ae_obj_crud({ data['super_key'] = key; data['jwt'] = jwt; - // NOTE: The key and or JWT should be in the header of the DELETE, GET, PATCH, POST - // This obj_v_name is the view name to use when returning data. Do not prefix it with v_. This is checked and done automatically by the API. - // This is not currently being exposed to other areas of the code. It is only used here. For now? - if (obj_v_name) { - obj_v_name = ''; - } + // V3 Standard: Unified endpoint for all objects + const endpoint = `/v3/crud/${obj_type}`; - let endpoint = ''; - if (obj_type == 'account') { - endpoint = `/crud/account`; - } else if (obj_type == 'activity_log') { - endpoint = `/crud/activity_log`; - } else if (obj_type == 'address') { - endpoint = `/crud/address`; - } else if (obj_type == 'archive') { - endpoint = `/crud/archive`; - } else if (obj_type == 'archive_content') { - endpoint = `/crud/archive/content`; - } else if (obj_type == 'contact') { - endpoint = `/crud/contact`; - } else if (obj_type == 'data_store') { - endpoint = `/crud/data_store`; - } else if (obj_type == 'event') { - endpoint = `/crud/event`; - } else if (obj_type == 'event_abstract') { - endpoint = `/crud/event/abstract`; - } else if (obj_type == 'event_badge') { - endpoint = `/crud/event/badge`; - } else if (obj_type == 'event_device') { - endpoint = `/crud/event/device`; - } else if (obj_type == 'event_exhibit') { - endpoint = `/crud/event/exhibit`; - } else if (obj_type == 'event_exhibit_tracking') { - endpoint = `/crud/event/exhibit/tracking`; - } else if (obj_type == 'event_file') { - endpoint = `/crud/event/file`; - } else if (obj_type == 'event_location') { - endpoint = `/crud/event/location`; - } else if (obj_type == 'event_person') { - endpoint = `/crud/event/person`; - } else if (obj_type == 'event_presentation') { - endpoint = `/crud/event/presentation`; - } else if (obj_type == 'event_presenter') { - endpoint = `/crud/event/presenter`; - // obj_v_name = 'event_presenter_soft_links'; - } else if (obj_type == 'event_session') { - endpoint = `/crud/event/session`; - } else if (obj_type == 'event_track') { - endpoint = `/crud/event/track`; - } else if (obj_type == 'grant') { - endpoint = `/crud/grant`; - } else if (obj_type == 'hosted_file') { - endpoint = `/crud/hosted_file`; - } else if (obj_type == 'journal') { - endpoint = `/crud/journal`; - } else if (obj_type == 'journal_entry') { - endpoint = `/crud/journal/entry`; - } else if (obj_type == 'order') { - endpoint = `/crud/order`; - } else if (obj_type == 'order_line') { - endpoint = `/crud/order/line`; - } else if (obj_type == 'page') { - endpoint = `/crud/page`; - } else if (obj_type == 'person') { - endpoint = `/crud/person`; - } else if (obj_type == 'post') { - endpoint = `/crud/post`; - } else if (obj_type == 'post_comment') { - endpoint = `/crud/post/comment`; - } else if (obj_type == 'sponsorship_cfg') { - endpoint = `/crud/sponsorship/cfg`; - } else if (obj_type == 'sponsorship') { - endpoint = `/crud/sponsorship`; - } else if (obj_type == 'site') { - endpoint = `/crud/site`; - // } else if (obj_type == 'user') { - // endpoint = `/crud/user`; - } else { - console.log(`Unknown object type: ${obj_type}`); - return false; - } if (log_lvl) { console.log('Endpoint:', endpoint); } @@ -315,86 +237,9 @@ export const update_ae_obj_id_crud = async function update_ae_obj_id_crud({ data['super_key'] = key; data['jwt'] = jwt; - // NOTE: The key and or JWT should be in the header of the DELETE, GET, PATCH, POST + // V3 Standard: Unified endpoint for all objects + const endpoint = `/v3/crud/${obj_type}/${obj_id}`; - // This obj_v_name is the view name to use when returning data. Do not prefix it with v_. This is checked and done automatically by the API. - // This is not currently being exposed to other areas of the code. It is only used here. For now? - if (obj_v_name) { - obj_v_name = ''; - } - - let endpoint = ''; - if (obj_type == 'account') { - endpoint = `/crud/account/${obj_id}`; - } else if (obj_type == 'address') { - endpoint = `/crud/address/${obj_id}`; - } else if (obj_type == 'archive') { - endpoint = `/crud/archive/${obj_id}`; - } else if (obj_type == 'archive_content') { - endpoint = `/crud/archive/content/${obj_id}`; - } else if (obj_type == 'contact') { - endpoint = `/crud/contact/${obj_id}`; - } else if (obj_type == 'data_store') { - endpoint = `/crud/data_store/${obj_id}`; - } else if (obj_type == 'event') { - endpoint = `/crud/event/${obj_id}`; - } else if (obj_type == 'event_abstract') { - endpoint = `/crud/event/abstract/${obj_id}`; - } else if (obj_type == 'event_badge') { - endpoint = `/crud/event/badge/${obj_id}`; - } else if (obj_type == 'event_device') { - endpoint = `/crud/event/device/${obj_id}`; - } else if (obj_type == 'event_exhibit') { - endpoint = `/crud/event/exhibit/${obj_id}`; - } else if (obj_type == 'event_exhibit_tracking') { - endpoint = `/crud/event/exhibit/tracking/${obj_id}`; - } else if (obj_type == 'event_file') { - endpoint = `/crud/event/file/${obj_id}`; - } else if (obj_type == 'event_location') { - endpoint = `/crud/event/location/${obj_id}`; - } else if (obj_type == 'event_person') { - endpoint = `/crud/event/person/${obj_id}`; - } else if (obj_type == 'event_presentation') { - endpoint = `/crud/event/presentation/${obj_id}`; - } else if (obj_type == 'event_presenter') { - endpoint = `/crud/event/presenter/${obj_id}`; - // obj_v_name = 'event_presenter_soft_links'; - } else if (obj_type == 'event_session') { - endpoint = `/crud/event/session/${obj_id}`; - } else if (obj_type == 'event_track') { - endpoint = `/crud/event/track/${obj_id}`; - } else if (obj_type == 'grant') { - endpoint = `/crud/grant/${obj_id}`; - } else if (obj_type == 'hosted_file') { - endpoint = `/crud/hosted_file/${obj_id}`; - } else if (obj_type == 'journal') { - endpoint = `/crud/journal/${obj_id}`; - } else if (obj_type == 'journal_entry') { - endpoint = `/crud/journal/entry/${obj_id}`; - } else if (obj_type == 'order') { - endpoint = `/crud/order/${obj_id}`; - } else if (obj_type == 'order_line') { - endpoint = `/crud/order/line/${obj_id}`; - } else if (obj_type == 'page') { - endpoint = `/crud/page/${obj_id}`; - } else if (obj_type == 'person') { - endpoint = `/crud/person/${obj_id}`; - } else if (obj_type == 'post') { - endpoint = `/crud/post/${obj_id}`; - } else if (obj_type == 'post_comment') { - endpoint = `/crud/post/comment/${obj_id}`; - } else if (obj_type == 'site') { - endpoint = `/crud/site/${obj_id}`; - } else if (obj_type == 'sponsorship_cfg') { - endpoint = `/crud/sponsorship/cfg/${obj_id}`; - } else if (obj_type == 'sponsorship') { - endpoint = `/crud/sponsorship/${obj_id}`; - // } else if (obj_type == 'user') { - // endpoint = `/crud/user/${obj_id}`; - } else { - console.log(`Unknown object type: ${obj_type}`); - return false; - } if (log_lvl) { console.log('Endpoint:', endpoint); } @@ -434,34 +279,6 @@ export const update_ae_obj_id_crud = async function update_ae_obj_id_crud({ } } - // If the data is an object then we need to loop through the object and convert any objects to JSON strings, but only if the property name ends with "_json". - // if (Array.isArray(data)) { - // // console.log('Data is an array'); - // for (let i = 0; i < data.length; i++) { - // // console.log(data[i]); - // if (typeof data[i] == 'object') { - // // console.log('Data is an object'); - // for (const [key, value] of Object.entries(data[i])) { - // // console.log(key, value); - // if (key.endsWith('_json')) { - // console.log(`${key}: ${value}`); - // data[i][key] = JSON.stringify(value); - // } - // } - - // } - // } - // } else if (typeof data == 'object') { - // // console.log('Data is an object'); - // for (const [key, value] of Object.entries(data)) { - // // console.log(key, value); - // if (key.endsWith('_json')) { - // console.log(`${key}: ${value}`); - // data[key] = JSON.stringify(value); - // } - // } - // } - if (log_lvl) { console.log('Data:', data); } @@ -521,77 +338,9 @@ export const delete_ae_obj_id_crud = async function delete_ae_obj_id_crud({ data['jwt'] = jwt; // NOTE: The key and or JWT should be in the header of the DELETE, GET, PATCH, POST - let endpoint = ''; - if (obj_type == 'account') { - endpoint = `/crud/account/${obj_id}`; - } else if (obj_type == 'address') { - endpoint = `/crud/address/${obj_id}`; - } else if (obj_type == 'archive') { - endpoint = `/crud/archive/${obj_id}`; - } else if (obj_type == 'archive_content') { - endpoint = `/crud/archive/content/${obj_id}`; - } else if (obj_type == 'contact') { - endpoint = `/crud/contact/${obj_id}`; - } else if (obj_type == 'data_store') { - endpoint = `/crud/data_store/${obj_id}`; - } else if (obj_type == 'event') { - endpoint = `/crud/event/${obj_id}`; - } else if (obj_type == 'event_abstract') { - endpoint = `/crud/event/abstract/${obj_id}`; - } else if (obj_type == 'event_badge') { - endpoint = `/crud/event/badge/${obj_id}`; - } else if (obj_type == 'event_device') { - endpoint = `/crud/event/device/${obj_id}`; - } else if (obj_type == 'event_exhibit') { - endpoint = `/crud/event/exhibit/${obj_id}`; - } else if (obj_type == 'event_exhibit_tracking') { - endpoint = `/crud/event/exhibit/tracking/${obj_id}`; - } else if (obj_type == 'event_file') { - endpoint = `/crud/event/file/${obj_id}`; - } else if (obj_type == 'event_location') { - endpoint = `/crud/event/location/${obj_id}`; - } else if (obj_type == 'event_person') { - endpoint = `/crud/event/person/${obj_id}`; - } else if (obj_type == 'event_presentation') { - endpoint = `/crud/event/presentation/${obj_id}`; - } else if (obj_type == 'event_presenter') { - endpoint = `/crud/event/presenter/${obj_id}`; - } else if (obj_type == 'event_session') { - endpoint = `/crud/event/session/${obj_id}`; - } else if (obj_type == 'event_track') { - endpoint = `/crud/event/track/${obj_id}`; - } else if (obj_type == 'grant') { - endpoint = `/crud/grant/${obj_id}`; - } else if (obj_type == 'hosted_file') { - endpoint = `/crud/hosted_file/${obj_id}`; - } else if (obj_type == 'journal') { - endpoint = `/crud/journal/${obj_id}`; - } else if (obj_type == 'journal_entry') { - endpoint = `/crud/journal/entry/${obj_id}`; - } else if (obj_type == 'order') { - endpoint = `/crud/order/${obj_id}`; - } else if (obj_type == 'order_line') { - endpoint = `/crud/order/line/${obj_id}`; - } else if (obj_type == 'page') { - endpoint = `/crud/page/${obj_id}`; - } else if (obj_type == 'person') { - endpoint = `/crud/person/${obj_id}`; - } else if (obj_type == 'post') { - endpoint = `/crud/post/${obj_id}`; - } else if (obj_type == 'post_comment') { - endpoint = `/crud/post/comment/${obj_id}`; - } else if (obj_type == 'site') { - endpoint = `/crud/site/${obj_id}`; - } else if (obj_type == 'sponsorship_cfg') { - endpoint = `/crud/sponsorship/cfg/${obj_id}`; - } else if (obj_type == 'sponsorship') { - endpoint = `/crud/sponsorship/${obj_id}`; - // } else if (obj_type == 'user') { - // endpoint = `/crud/user/${obj_id}`; - } else { - console.log(`Unknown object type: ${obj_type}`); - return false; - } + // V3 Standard: Unified endpoint for all objects + const endpoint = `/v3/crud/${obj_type}/${obj_id}`; + if (log_lvl) { console.log('Endpoint:', endpoint); } @@ -620,6 +369,7 @@ export const delete_ae_obj_id_crud = async function delete_ae_obj_id_crud({ return object_obj_delete_promise; }; + /* BEGIN: Hosted File Related */ // Updated 2026-01-07 diff --git a/src/lib/stores/ae_stores.ts b/src/lib/stores/ae_stores.ts index c8fe67fd..989df159 100644 --- a/src/lib/stores/ae_stores.ts +++ b/src/lib/stores/ae_stores.ts @@ -457,11 +457,8 @@ export const ae_api_data_struct: key_val = { }; const ae_api_headers: key_val = {}; -ae_api_headers['Access-Control-Allow-Origin'] = '*'; -ae_api_headers['Content-Yype'] = 'application/json'; +ae_api_headers['Content-Type'] = 'application/json'; ae_api_headers['x-aether-api-key'] = ae_api_data_struct.api_secret_key; -ae_api_headers['x-aether-api-token'] = 'fake-temp-token'; -ae_api_headers['x-aether-api-expire-on'] = ''; if (ae_account_id) { ae_api_headers['x-account-id'] = ae_account_id; } else {