feat(web): auto-logout on 401 inside apiFetch
Dynamic import breaks the apiFetch <-> auth/store cycle. silent:true prevents re-POSTing /logout against an already-dead session.
This commit is contained in:
@@ -79,3 +79,24 @@ describe('api.get/post/del', () => {
|
|||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('apiFetch 401 interceptor', () => {
|
||||||
|
test('401 response triggers auth.logout({silent:true}) and still throws', async () => {
|
||||||
|
const logoutSpy = vi.fn();
|
||||||
|
vi.doMock('$lib/auth/store.svelte', () => ({
|
||||||
|
logout: logoutSpy,
|
||||||
|
login: vi.fn(),
|
||||||
|
bootstrap: vi.fn(),
|
||||||
|
user: { value: null }
|
||||||
|
}));
|
||||||
|
// Re-import to pick up the mock.
|
||||||
|
const { apiFetch: apiFetchFresh } = await import('./client');
|
||||||
|
stubFetch(401, { error: { code: 'unauthorized', message: 'session expired' } });
|
||||||
|
await expect(apiFetchFresh('/api/me')).rejects.toMatchObject({
|
||||||
|
code: 'unauthorized',
|
||||||
|
status: 401
|
||||||
|
});
|
||||||
|
expect(logoutSpy).toHaveBeenCalledWith({ silent: true });
|
||||||
|
vi.doUnmock('$lib/auth/store.svelte');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ export async function apiFetch(path: string, init?: RequestInit): Promise<unknow
|
|||||||
});
|
});
|
||||||
const body = res.status === 204 ? null : await res.json().catch(() => null);
|
const body = res.status === 204 ? null : await res.json().catch(() => null);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
if (res.status === 401) {
|
||||||
|
// Lazy import: auth/store imports from this file, so a top-level
|
||||||
|
// import would be circular. By the time any 401 actually happens
|
||||||
|
// at runtime, both modules have finished loading.
|
||||||
|
const { logout } = await import('$lib/auth/store.svelte');
|
||||||
|
await logout({ silent: true });
|
||||||
|
}
|
||||||
const envelope = body && (body as { error?: { code?: string; message?: string } }).error;
|
const envelope = body && (body as { error?: { code?: string; message?: string } }).error;
|
||||||
const err: ApiError = {
|
const err: ApiError = {
|
||||||
code: envelope?.code ?? 'unknown',
|
code: envelope?.code ?? 'unknown',
|
||||||
|
|||||||
Reference in New Issue
Block a user