feat: session-expired banner via ae_auth_error store
- Add ae_auth_error writable store to ae_stores.ts - Wire api_get_object, api_post_object, api_patch_object to set ae_auth_error on 401/403 (browser-only guard, never fires SSR) - Root layout watches ae_auth_error; only raises flag_expired when a JWT is present (prevents false trigger on unauthenticated loads) - Dismissible amber banner added to root layout (non-blocking, above content) - Tested via debug menu trigger; banner fires and clears correctly
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { ae_auth_error } from '$lib/stores/ae_stores';
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
|
||||
export let temp_get_blob_percent_completed = 0;
|
||||
@@ -95,8 +97,8 @@ export const get_object = async function get_object({
|
||||
|
||||
// Handle "Bootstrap Paradox" for unauthenticated requests
|
||||
const bypass_val = merged_headers['x-no-account-id'] || merged_headers['x_no_account_id'];
|
||||
const is_valid_bypass = bypass_val === 'bypass' ||
|
||||
bypass_val === 'Nothing to See Here' ||
|
||||
const is_valid_bypass = bypass_val === 'bypass' ||
|
||||
bypass_val === 'Nothing to See Here' ||
|
||||
params['key'] ||
|
||||
bypass_val === 'direct-download';
|
||||
|
||||
@@ -105,7 +107,7 @@ export const get_object = async function get_object({
|
||||
delete merged_headers['x-account-id'];
|
||||
delete merged_headers['x_account_id'];
|
||||
} else {
|
||||
// If it's a placeholder (like "No_Account_ID_Here"), just remove the bypass header
|
||||
// If it's a placeholder (like "No_Account_ID_Here"), just remove the bypass header
|
||||
// but DO NOT strip the valid Account ID.
|
||||
delete merged_headers['x-no-account-id'];
|
||||
delete merged_headers['x_no_account_id'];
|
||||
@@ -116,7 +118,7 @@ export const get_object = async function get_object({
|
||||
const prop_cleaned = prop.replaceAll('_', '-');
|
||||
let value = merged_headers[prop];
|
||||
if (value === null || value === undefined) continue;
|
||||
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
@@ -124,10 +126,10 @@ export const get_object = async function get_object({
|
||||
}
|
||||
|
||||
// Auto-inject Authorization header if JWT is present but header is missing
|
||||
let jwt = headers_cleaned['jwt'] ||
|
||||
headers_cleaned['JWT'] ||
|
||||
api_cfg['jwt'] ||
|
||||
api_cfg['headers']?.['jwt'] ||
|
||||
let jwt = headers_cleaned['jwt'] ||
|
||||
headers_cleaned['JWT'] ||
|
||||
api_cfg['jwt'] ||
|
||||
api_cfg['headers']?.['jwt'] ||
|
||||
api_cfg['headers']?.['JWT'];
|
||||
|
||||
// Final Fallback: Direct check of primary ae_loc key
|
||||
@@ -142,7 +144,7 @@ export const get_object = async function get_object({
|
||||
// Silently fail on storage read
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (jwt && !headers_cleaned['Authorization'] && !headers_cleaned['authorization']) {
|
||||
headers_cleaned['Authorization'] = `Bearer ${jwt}`;
|
||||
}
|
||||
@@ -200,9 +202,9 @@ export const get_object = async function get_object({
|
||||
if (response instanceof Error || (response && (response.name === 'TypeError' || response.name === 'AbortError'))) {
|
||||
// If it was an explicit abort, definitely stop
|
||||
if (response.name === 'AbortError') return false;
|
||||
|
||||
|
||||
if (log_lvl > 1) console.log('API GET Object: Detected NetworkError or TypeError. Failing fast.');
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
@@ -237,7 +239,7 @@ export const get_object = async function get_object({
|
||||
// FAIL FAST (Section 2D): Do not retry on Auth or Client errors (400, 401, 403, 422)
|
||||
if (response.status === 400 || response.status === 401 || response.status === 403 || response.status === 422) {
|
||||
if (log_lvl) console.error(`API Client Failure (${response.status}). Failing fast.`);
|
||||
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.warn(`AUTH DIAGNOSTICS: Headers sent for ${endpoint}:`, {
|
||||
has_auth: !!headers_cleaned['Authorization'],
|
||||
@@ -245,8 +247,10 @@ export const get_object = async function get_object({
|
||||
has_account_id: !!headers_cleaned['x-account-id'],
|
||||
jwt_preview: jwt ? `${jwt.slice(0, 8)}...` : 'MISSING'
|
||||
});
|
||||
// Signal the root layout to show the session-expired banner.
|
||||
if (browser) ae_auth_error.set({ type: 'expired', ts: Date.now() });
|
||||
}
|
||||
|
||||
|
||||
// Structured Error Handling (V3): Attempt to get rich error metadata
|
||||
let error_json: any = null;
|
||||
try {
|
||||
@@ -256,9 +260,9 @@ export const get_object = async function get_object({
|
||||
}
|
||||
|
||||
if (log_lvl) console.log('The response was not ok. Structured Error Check:', error_json);
|
||||
|
||||
|
||||
if (error_json?.meta?.details) {
|
||||
return error_json;
|
||||
return error_json;
|
||||
}
|
||||
|
||||
// Fallback for standard FastAPI "detail" errors
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { ae_auth_error } from '$lib/stores/ae_stores';
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
|
||||
/**
|
||||
@@ -74,8 +76,8 @@ export const patch_object = async function patch_object({
|
||||
|
||||
// Handle "Bootstrap Paradox" for unauthenticated requests
|
||||
const bypass_val = merged_headers['x-no-account-id'] || merged_headers['x_no_account_id'];
|
||||
const is_valid_bypass = bypass_val === 'bypass' ||
|
||||
bypass_val === 'Nothing to See Here' ||
|
||||
const is_valid_bypass = bypass_val === 'bypass' ||
|
||||
bypass_val === 'Nothing to See Here' ||
|
||||
params['key'] ||
|
||||
bypass_val === 'direct-download';
|
||||
|
||||
@@ -84,7 +86,7 @@ export const patch_object = async function patch_object({
|
||||
delete merged_headers['x-account-id'];
|
||||
delete merged_headers['x_account_id'];
|
||||
} else {
|
||||
// If it's a placeholder (like "No_Account_ID_Here"), just remove the bypass header
|
||||
// If it's a placeholder (like "No_Account_ID_Here"), just remove the bypass header
|
||||
// but DO NOT strip the valid Account ID.
|
||||
delete merged_headers['x-no-account-id'];
|
||||
delete merged_headers['x_no_account_id'];
|
||||
@@ -102,10 +104,10 @@ export const patch_object = async function patch_object({
|
||||
}
|
||||
|
||||
// Auto-inject Authorization header if JWT is present but header is missing
|
||||
let jwt = headers_cleaned['jwt'] ||
|
||||
headers_cleaned['JWT'] ||
|
||||
api_cfg['jwt'] ||
|
||||
api_cfg['headers']?.['jwt'] ||
|
||||
let jwt = headers_cleaned['jwt'] ||
|
||||
headers_cleaned['JWT'] ||
|
||||
api_cfg['jwt'] ||
|
||||
api_cfg['headers']?.['jwt'] ||
|
||||
api_cfg['headers']?.['JWT'];
|
||||
|
||||
// Final Fallback: Direct check of primary ae_loc key
|
||||
@@ -185,7 +187,7 @@ export const patch_object = async function patch_object({
|
||||
// FAIL FAST (Section 2D): Do not retry on Auth or Client errors (400, 401, 403, 422)
|
||||
if (response.status === 400 || response.status === 401 || response.status === 403 || response.status === 422) {
|
||||
if (log_lvl) console.error(`API Client Failure (${response.status}). Failing fast.`);
|
||||
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.warn(`AUTH DIAGNOSTICS (PATCH): Headers sent for ${endpoint}:`, {
|
||||
has_auth: !!headers_cleaned['Authorization'],
|
||||
@@ -193,6 +195,8 @@ export const patch_object = async function patch_object({
|
||||
has_account_id: !!headers_cleaned['x-account-id'],
|
||||
jwt_preview: jwt ? `${jwt.slice(0, 8)}...` : 'MISSING'
|
||||
});
|
||||
// Signal the root layout to show the session-expired banner.
|
||||
if (browser) ae_auth_error.set({ type: 'expired', ts: Date.now() });
|
||||
}
|
||||
|
||||
// Structured Error Handling (V3): Attempt to get rich error metadata
|
||||
@@ -204,9 +208,9 @@ export const patch_object = async function patch_object({
|
||||
}
|
||||
|
||||
if (log_lvl) console.log('The response was not ok. Structured Error Check:', error_json);
|
||||
|
||||
|
||||
if (error_json?.meta?.details) {
|
||||
return error_json;
|
||||
return error_json;
|
||||
}
|
||||
|
||||
// Fallback for standard FastAPI "detail" errors
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { ae_auth_error } from '$lib/stores/ae_stores';
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
|
||||
export const temp_post_blob_percent_completed = 0;
|
||||
@@ -94,8 +96,8 @@ export const post_object = async function post_object({
|
||||
|
||||
// Handle "Bootstrap Paradox" for unauthenticated requests
|
||||
const bypass_val = merged_headers['x-no-account-id'] || merged_headers['x_no_account_id'];
|
||||
const is_valid_bypass = bypass_val === 'bypass' ||
|
||||
bypass_val === 'Nothing to See Here' ||
|
||||
const is_valid_bypass = bypass_val === 'bypass' ||
|
||||
bypass_val === 'Nothing to See Here' ||
|
||||
params['key'] ||
|
||||
bypass_val === 'direct-download';
|
||||
|
||||
@@ -104,7 +106,7 @@ export const post_object = async function post_object({
|
||||
delete merged_headers['x-account-id'];
|
||||
delete merged_headers['x_account_id'];
|
||||
} else {
|
||||
// If it's a placeholder (like "No_Account_ID_Here"), just remove the bypass header
|
||||
// If it's a placeholder (like "No_Account_ID_Here"), just remove the bypass header
|
||||
// but DO NOT strip the valid Account ID.
|
||||
delete merged_headers['x-no-account-id'];
|
||||
delete merged_headers['x_no_account_id'];
|
||||
@@ -122,10 +124,10 @@ export const post_object = async function post_object({
|
||||
}
|
||||
|
||||
// Auto-inject Authorization header if JWT is present but header is missing
|
||||
let jwt = headers_cleaned['jwt'] ||
|
||||
headers_cleaned['JWT'] ||
|
||||
api_cfg['jwt'] ||
|
||||
api_cfg['headers']?.['jwt'] ||
|
||||
let jwt = headers_cleaned['jwt'] ||
|
||||
headers_cleaned['JWT'] ||
|
||||
api_cfg['jwt'] ||
|
||||
api_cfg['headers']?.['jwt'] ||
|
||||
api_cfg['headers']?.['JWT'];
|
||||
|
||||
// Final Fallback: Direct check of primary ae_loc key
|
||||
@@ -207,7 +209,7 @@ export const post_object = async function post_object({
|
||||
if (response instanceof Error || (response && (response.name === 'TypeError' || response.name === 'AbortError'))) {
|
||||
if (response.name === 'AbortError') return false;
|
||||
if (log_lvl > 1) console.log('API POST Object: Detected NetworkError or TypeError. Failing fast.');
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
@@ -231,7 +233,7 @@ export const post_object = async function post_object({
|
||||
// FAIL FAST (Section 2D): Do not retry on Auth or Client errors (400, 401, 403, 422)
|
||||
if (response.status === 400 || response.status === 401 || response.status === 403 || response.status === 422) {
|
||||
if (log_lvl) console.error(`API Client Failure (${response.status}). Failing fast.`);
|
||||
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.warn(`AUTH DIAGNOSTICS (POST): Headers sent for ${endpoint}:`, {
|
||||
has_auth: !!headers_cleaned['Authorization'],
|
||||
@@ -239,8 +241,10 @@ export const post_object = async function post_object({
|
||||
has_account_id: !!headers_cleaned['x-account-id'],
|
||||
jwt_preview: jwt ? `${jwt.slice(0, 8)}...` : 'MISSING'
|
||||
});
|
||||
// Signal the root layout to show the session-expired banner.
|
||||
if (browser) ae_auth_error.set({ type: 'expired', ts: Date.now() });
|
||||
}
|
||||
|
||||
|
||||
// Structured Error Handling (V3): Attempt to get rich error metadata
|
||||
let error_json: any = null;
|
||||
try {
|
||||
@@ -250,9 +254,9 @@ export const post_object = async function post_object({
|
||||
}
|
||||
|
||||
if (log_lvl) console.log('The response was not ok. Structured Error Check:', error_json);
|
||||
|
||||
|
||||
if (error_json?.meta?.details) {
|
||||
return error_json;
|
||||
return error_json;
|
||||
}
|
||||
|
||||
// Fallback for standard FastAPI "detail" errors
|
||||
|
||||
@@ -530,6 +530,10 @@ export const slct = writable(slct_obj_template);
|
||||
export const slct_trigger: any = writable(null);
|
||||
// console.log(`AE Stores - Selected Trigger:`, slct_trigger);
|
||||
|
||||
// Auth error signal — set by API helpers on 401/403 to trigger the session-expired banner in the root layout.
|
||||
// Only set from browser context (never SSR). 'expired' covers both 401 and 403 responses.
|
||||
export const ae_auth_error = writable<{ type: 'expired' | null; ts: number | null }>({ type: null, ts: null });
|
||||
|
||||
/* *** BEGIN *** Create time variable */
|
||||
// Updated 2020
|
||||
export const time = readable(new Date(), function start(set) {
|
||||
|
||||
Reference in New Issue
Block a user