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.
This commit is contained in:
Scott Idem
2026-02-13 19:10:32 -05:00
parent 3e83890932
commit f62bd9fb79
7 changed files with 68 additions and 313 deletions

View File

@@ -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<string, string> = {
'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);

View File

@@ -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

View File

@@ -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

View File

@@ -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