fix(core): preserve account context on key params and harden account detail fallback
- api_get/post/patch_object: stop treating params.key as account-bypass trigger\n- account detail: remove forced key usage, add list/cache fallback path\n- account detail: fix fallback bug that set load_error even when fallback record existed\n- sites detail: pretty-print cfg_json before save\n- docs: clarify key != bypass and add 403 troubleshooting notes
This commit is contained in:
@@ -206,6 +206,10 @@ x-aether-api-key: <PUBLIC_AE_API_SECRET_KEY>
|
||||
x-account-id: <account_id>
|
||||
```
|
||||
|
||||
**Do not treat `params.key` as an auth bypass.**
|
||||
Only explicit `x-no-account-id: bypass` means "drop account context".
|
||||
If `key` is present for business logic, keep `x-account-id` intact.
|
||||
|
||||
### Dexie queries — always use the object ID index, not `.get()`
|
||||
All `db_core` (and other module) Dexie tables define their schema with `id` as the first
|
||||
field (primary key), followed by the object's string ID (e.g. `person_id`). V3 **never**
|
||||
@@ -288,6 +292,10 @@ These are real incidents — know them before you start.
|
||||
clean of data loads in private modules. See `GUIDE__SvelteKit2_Svelte5_DexieJS.md` →
|
||||
"SvelteKit Layout Hierarchy: Security and Execution Order" for the full explanation.
|
||||
|
||||
10. **Using query `key` as a proxy for bypass stripped `x-account-id`** — this caused
|
||||
valid account-scoped requests to lose account context and 403. `key` can be a valid
|
||||
endpoint/business param, but it is not equivalent to `x-no-account-id: bypass`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Source Layout (Quick Reference)
|
||||
|
||||
@@ -21,6 +21,9 @@ Required for any non-public data (Journals, Badges, Users, etc.).
|
||||
* **Header:** `x-no-account-id: bypass`
|
||||
3. **Token Access**: Provide a **JWT** in the query string.
|
||||
* **Query Param:** `?jwt=<token>`
|
||||
4. **Important Distinction:** A query parameter named `key` is **not** an account-context bypass signal.
|
||||
* `key` may be used by specific endpoints/business logic, but it must **not** cause the frontend to remove `x-account-id`.
|
||||
* Only explicit `x-no-account-id: bypass` should strip account context.
|
||||
|
||||
> [!CAUTION]
|
||||
> **UNSUPPORTED HEADERS:** The header `x-aether-api-token` is **NOT recognized** by the V3 API. If you send it, the backend will treat you as a guest and block access to private data.
|
||||
@@ -587,3 +590,5 @@ If you receive a 403 on a valid ID:
|
||||
2. Ensure you are sending `x-account-id` and NOT `x-aether-api-token`.
|
||||
3. Verify the record actually belongs to the account ID you are sending.
|
||||
4. Check if the object is marked `public_read: True` in the registry. (Posts and Archive Content allow guest access; Journals and Badges do not).
|
||||
5. Confirm the frontend is not treating `params.key` as an implicit bypass and stripping `x-account-id`.
|
||||
6. If list/search endpoints work but `GET /v3/crud/{obj_type}/{id}` still returns 403, this is likely endpoint-level policy (e.g., requires stronger auth like JWT) rather than a transport/header bug.
|
||||
|
||||
@@ -108,7 +108,6 @@ export const get_object = async function get_object({
|
||||
const is_valid_bypass =
|
||||
bypass_val === 'bypass' ||
|
||||
bypass_val === 'Nothing to See Here' ||
|
||||
params['key'] ||
|
||||
bypass_val === 'direct-download';
|
||||
|
||||
if (is_valid_bypass) {
|
||||
|
||||
@@ -82,7 +82,6 @@ export const patch_object = async function patch_object({
|
||||
const is_valid_bypass =
|
||||
bypass_val === 'bypass' ||
|
||||
bypass_val === 'Nothing to See Here' ||
|
||||
params['key'] ||
|
||||
bypass_val === 'direct-download';
|
||||
|
||||
if (is_valid_bypass) {
|
||||
|
||||
@@ -104,7 +104,6 @@ export const post_object = async function post_object({
|
||||
const is_valid_bypass =
|
||||
bypass_val === 'bypass' ||
|
||||
bypass_val === 'Nothing to See Here' ||
|
||||
params['key'] ||
|
||||
bypass_val === 'direct-download';
|
||||
|
||||
if (is_valid_bypass) {
|
||||
|
||||
@@ -3,9 +3,11 @@ import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import {
|
||||
load_ae_obj_id__account,
|
||||
load_ae_obj_li__account,
|
||||
update_ae_obj__account,
|
||||
delete_ae_obj_id__account
|
||||
} from '$lib/ae_core/ae_core__account';
|
||||
import { db_core } from '$lib/ae_core/db_core';
|
||||
import { editable_fields__account } from '$lib/ae_core/ae_core__account.editable_fields';
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -24,15 +26,60 @@ let loading = $state(true);
|
||||
let saving = $state(false);
|
||||
let save_success = $state(false);
|
||||
let confirm_action = $state<string | null>(null);
|
||||
let load_error = $state<string | null>(null);
|
||||
let loaded_from_cache = $state(false);
|
||||
|
||||
async function load_account() {
|
||||
loading = true;
|
||||
account = await load_ae_obj_id__account({
|
||||
api_cfg: $ae_api,
|
||||
account_id,
|
||||
log_lvl: 1
|
||||
});
|
||||
loading = false;
|
||||
load_error = null;
|
||||
loaded_from_cache = false;
|
||||
try {
|
||||
account = await load_ae_obj_id__account({
|
||||
api_cfg: $ae_api,
|
||||
account_id,
|
||||
log_lvl: 1
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
const account_li = await load_ae_obj_li__account({
|
||||
api_cfg: $ae_api,
|
||||
enabled: 'all',
|
||||
hidden: 'all',
|
||||
try_cache: false,
|
||||
log_lvl: 0
|
||||
});
|
||||
|
||||
const account_from_list =
|
||||
account_li?.find((a: any) => a.account_id === account_id) ??
|
||||
null;
|
||||
|
||||
if (account_from_list) {
|
||||
account = account_from_list;
|
||||
loaded_from_cache = true;
|
||||
}
|
||||
|
||||
const cached_account = await db_core.account
|
||||
.where('account_id')
|
||||
.equals(account_id)
|
||||
.first();
|
||||
|
||||
if (!account && cached_account) {
|
||||
account = cached_account;
|
||||
loaded_from_cache = true;
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
load_error =
|
||||
'Unable to load this account. Authentication or permissions may be missing for this record.';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load account:', error);
|
||||
load_error =
|
||||
'Unable to load this account due to a network or API error. Please retry.';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -40,6 +87,7 @@ onMount(() => {
|
||||
goto('/core');
|
||||
return;
|
||||
}
|
||||
|
||||
load_account();
|
||||
});
|
||||
|
||||
@@ -151,7 +199,36 @@ async function handle_delete() {
|
||||
<div class="placeholder h-full w-full animate-pulse rounded-2xl">
|
||||
</div>
|
||||
</div>
|
||||
{:else if load_error}
|
||||
<div
|
||||
class="card border-error-500/30 bg-error-500/5 flex flex-col items-start gap-3 border p-6 shadow-xl">
|
||||
<h3 class="h4 font-black text-error-600">Account Load Failed</h3>
|
||||
<p class="text-sm opacity-80">{load_error}</p>
|
||||
{#if !$ae_loc.jwt}
|
||||
<p class="text-xs opacity-70">
|
||||
This session is using manager passcode access (no JWT). Some account-by-id API routes may require stronger auth than list views.
|
||||
</p>
|
||||
{/if}
|
||||
<div class="flex flex-wrap gap-2 pt-1">
|
||||
<button
|
||||
class="btn btn-sm preset-filled-primary"
|
||||
onclick={load_account}>
|
||||
Retry
|
||||
</button>
|
||||
<a class="btn btn-sm preset-tonal-surface" href="/core/accounts">
|
||||
Back to Accounts
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else if account}
|
||||
{#if loaded_from_cache}
|
||||
<div
|
||||
class="card border-warning-500/30 bg-warning-500/5 mb-2 flex items-start gap-3 border p-4 shadow-sm">
|
||||
<p class="text-sm opacity-80">
|
||||
This record loaded from local cache because direct account API access is currently restricted for this session.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div
|
||||
class="card preset-tonal-surface border-surface-500/10 space-y-4 border p-6 shadow-xl">
|
||||
|
||||
@@ -70,8 +70,8 @@ async function handle_save_site() {
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure cfg_json is included explicitly
|
||||
data_kv.cfg_json = site.cfg_json;
|
||||
// Pretty-print cfg_json so it's human-readable in the DB (TEXT column)
|
||||
data_kv.cfg_json = JSON.stringify(site.cfg_json, null, 2);
|
||||
|
||||
await update_ae_obj__site({
|
||||
api_cfg: $ae_api,
|
||||
|
||||
Reference in New Issue
Block a user