When a session token expires, API calls return 401 but nothing caught it — errors were shown as generic dialogs or swallowed. Now both apiClient and streamingService detect 401 responses and clear auth state, which causes App.tsx to render the login modal automatically.
83 lines
1.9 KiB
TypeScript
83 lines
1.9 KiB
TypeScript
// Generic API client with authentication
|
|
|
|
import type { AuthUser } from '@/auth/types';
|
|
import { ApiError } from '@/types';
|
|
|
|
let onUnauthorized: (() => void) | null = null;
|
|
|
|
export function setOnUnauthorized(handler: () => void): void {
|
|
onUnauthorized = handler;
|
|
}
|
|
|
|
export function fireUnauthorized(): void {
|
|
if (onUnauthorized) {
|
|
onUnauthorized();
|
|
}
|
|
}
|
|
|
|
export interface RequestOptions {
|
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
params?: Record<string, string | number | boolean | Date | undefined>;
|
|
body?: unknown;
|
|
}
|
|
|
|
/**
|
|
* Build query string from parameters object
|
|
*/
|
|
function buildQueryString(params: Record<string, string | number | boolean | Date | undefined>): string {
|
|
const queryString = new URLSearchParams();
|
|
|
|
for (const [key, value] of Object.entries(params)) {
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
if (value instanceof Date) {
|
|
queryString.append(key, value.toISOString());
|
|
} else {
|
|
queryString.append(key, String(value));
|
|
}
|
|
}
|
|
}
|
|
|
|
return queryString.toString();
|
|
}
|
|
|
|
/**
|
|
* Generic authenticated API request
|
|
*/
|
|
export async function apiRequest<T>(
|
|
user: AuthUser,
|
|
endpoint: string,
|
|
options: RequestOptions = {}
|
|
): Promise<T> {
|
|
const { method = 'GET', params, body } = options;
|
|
|
|
let url = endpoint;
|
|
if (params) {
|
|
const queryString = buildQueryString(params);
|
|
if (queryString) {
|
|
url = `${endpoint}?${queryString}`;
|
|
}
|
|
}
|
|
|
|
const fetchOptions: RequestInit = {
|
|
method,
|
|
headers: {
|
|
Authorization: `Bearer ${user.accessToken}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
};
|
|
|
|
if (body !== undefined) {
|
|
fetchOptions.body = JSON.stringify(body);
|
|
}
|
|
|
|
const response = await fetch(url, fetchOptions);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
fireUnauthorized();
|
|
}
|
|
throw new ApiError(`Error: ${response.status}`, response.status);
|
|
}
|
|
|
|
return response.json() as Promise<T>;
|
|
}
|