Public Access
feat(auth): use the cookie session and refresh on 401
The token is no longer stored; a tbp_session flag remembers the login. apiFetch renews the session once on a 401 and sends the request again. Parallel requests and other tabs share one refresh, because Auth revokes the session when a refresh token is used twice. Only a 401 from the refresh ends the session; a 502 or no network shows the normal error.
This commit is contained in:
1 parent
5ff8d48c53
commit
d9a39dda22
34 files changed
+297
-147
No files matched your search
+4
-7
@@ -10,9 +10,6 @@ vi.mock('./hooks/useLibrarySync.js', () => ({default: () => ({status: 'OK', last
|
||||
vi.mock('./hooks/useForceSync.js', () => ({default: () => ({trigger: vi.fn(), busy: false})}));
|
||||
|
||||
const DEBOUNCE_WAIT_MS = 700;
|
||||
const b64 = (o) => btoa(JSON.stringify(o)).replace(/=+$/, '');
|
||||
// Expired by the browser clock. A clock that runs ahead of the server makes the backend still accept it.
|
||||
const EXPIRED_BY_LOCAL_CLOCK = `${b64({alg: 'EdDSA'})}.${b64({exp: Math.floor(Date.now() / 1000) - 60})}.sig`;
|
||||
const page = {content: [{appid: 220, name: 'Half-Life 2', playtimeMinutes: 0, status: 'BACKLOG'}], page: 0, size: 20, totalElements: 1, totalPages: 1};
|
||||
|
||||
const wait = (ms) => act(() => new Promise((resolve) => setTimeout(resolve, ms)));
|
||||
@@ -25,7 +22,7 @@ function Spy() {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('tbp_jwt', EXPIRED_BY_LOCAL_CLOCK);
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
globalThis.fetch = vi.fn(async () => ({ok: true, status: 200, json: async () => page}));
|
||||
});
|
||||
|
||||
@@ -35,14 +32,13 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('App', () => {
|
||||
it('keeps a token that only the browser clock calls expired; a 401 from the backend ends the session', async () => {
|
||||
it('ends the session on a 401 that the refresh cannot fix', async () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/library']}>
|
||||
<PreferencesProvider><App/><Spy/></PreferencesProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
await waitFor(() => expect(screen.getByText('Half-Life 2')).toBeTruthy());
|
||||
expect(localStorage.getItem('tbp_jwt')).toBe(EXPIRED_BY_LOCAL_CLOCK);
|
||||
|
||||
fetch.mockImplementation(async () => ({ok: false, status: 401, json: async () => ({})}));
|
||||
fireEvent.change(screen.getByRole('searchbox'), {target: {value: 'half'}});
|
||||
@@ -50,6 +46,7 @@ describe('App', () => {
|
||||
|
||||
await waitFor(() => expect(current.location.pathname).toBe('/login'));
|
||||
expect(current.location.state).toEqual({sessionExpired: true, from: '/library?q=half&page=0'});
|
||||
expect(localStorage.getItem('tbp_jwt')).toBeNull();
|
||||
expect(localStorage.getItem('tbp_session')).toBeNull();
|
||||
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/api/auth/refresh'), expect.objectContaining({method: 'POST'}));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user