feat(frontend): add apiFetch wrapper for automatic auth token injection

Centralizes auth token management into a reusable apiFetch() helper and getAuthHeaders() utility, eliminating inline Bearer token logic across all frontend files.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-07-06 19:21:34 +01:00
co-authored by Sisyphus
parent e831953e5b
commit 92124158bf
48 changed files with 530 additions and 1190 deletions
@@ -5,6 +5,7 @@
import { cubicOut } from 'svelte/easing';
import { browser } from '$app/environment';
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { Skeleton } from '$lib/components/ui/skeleton';
import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox';
@@ -138,9 +139,7 @@
per_page: perPage.toString(),
include_acknowledged: includeAcknowledged.toString()
});
const response = await fetch(`/api/admin/notifications?${params}`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const response = await apiFetch(`/api/admin/notifications?${params}`);
if (!response.ok) {
throw new Error(`Failed: ${response.status}`);
}
@@ -155,21 +154,16 @@
}
async function fetchBookingDetails(bookingId: string): Promise<Booking | null> {
const response = await fetch(`/api/admin/bookings/${bookingId}`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const response = await apiFetch(`/api/admin/bookings/${bookingId}`);
if (!response.ok) return null;
return response.json();
}
async function handleAction(notification: Notification) {
if (!notification.acknowledged_at) {
await fetch(`/api/admin/notifications/${notification.id}/acknowledge`, {
await apiFetch(`/api/admin/notifications/${notification.id}/acknowledge`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
});
}
@@ -192,11 +186,8 @@
}
} else if (action === 'edit_approve' && notification.booking_id) {
try {
const response = await fetch(
`/api/admin/bookings/${notification.booking_id}/edit-request`,
{
headers: { Authorization: `Bearer ${authStore.currentToken}` }
}
const response = await apiFetch(
`/api/admin/bookings/${notification.booking_id}/edit-request`
);
if (response.ok) {
const data = await response.json();
@@ -219,12 +210,9 @@
async function handleAcknowledge(notification: Notification) {
try {
const response = await fetch(`/api/admin/notifications/${notification.id}/acknowledge`, {
const response = await apiFetch(`/api/admin/notifications/${notification.id}/acknowledge`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
toast.error('Failed to acknowledge notification');
@@ -351,6 +339,10 @@
<svelte:head>
<script>
(function () {
// Pre-hydration auth guard: reads localStorage directly because the Svelte
// authStore hasn't initialized yet at this point (async+$state). This runs
// synchronously in <svelte:head> before any rendering, preventing a flash
// of protected content. The authStore handles post-hydration auth.
try {
var token = localStorage.getItem('authToken');
if (!token) {