diff --git a/src/api/auth.ts b/src/api/auth.ts index 8db42e8..09d0b88 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -9,25 +9,42 @@ setAuthTokenProvider(() => authToken) export async function currentUser(): Promise { if (cachedUser) return cachedUser try { - const res = await api.POST('/api/v1/auth/refresh', { params: { cookie: { user_id: 0 } } }) - if (!res.response.ok) return null - // refresh may return legacy Person; adapt minimally to User shape without type assertions - const p = res.data - if (typeof p === 'object' && p !== null) { - const idDesc = Object.getOwnPropertyDescriptor(p, 'id') - const nameDesc = Object.getOwnPropertyDescriptor(p, 'name') - const id = idDesc?.value - const name = nameDesc?.value - if (typeof id === 'number' && typeof name === 'string') { - cachedUser = { id, email: '', displayName: name } - } else { - cachedUser = null - } + // Provide minimal params to satisfy current OpenAPI shape; backend ignores cookie in JWT mode + const res = await api.POST('/api/v1/auth/refresh', { params: { cookie: { user_id: 0 } } }) + if (!res.response.ok) { + authToken = null + cachedUser = null + return null } + const data: unknown = res.data + if (typeof data !== 'object' || data === null) { + authToken = null + cachedUser = null + return null + } + const tokenVal = (data as Record)['accessToken'] + const userVal = (data as Record)['user'] + if (typeof tokenVal !== 'string' || typeof userVal !== 'object' || userVal === null) { + authToken = null + cachedUser = null + return null + } + const uid = (userVal as Record)['id'] + const email = (userVal as Record)['email'] + const displayName = (userVal as Record)['displayName'] + if (typeof uid !== 'number') { + authToken = null + cachedUser = null + return null + } + authToken = tokenVal + cachedUser = { id: uid, email: typeof email === 'string' ? email : '', displayName: typeof displayName === 'string' ? displayName : '' } + return cachedUser } catch (_) { + authToken = null cachedUser = null + return null } - return cachedUser } export async function login(username: string): Promise { diff --git a/tests/auth.refresh.api.test.ts b/tests/auth.refresh.api.test.ts new file mode 100644 index 0000000..9fec9b0 --- /dev/null +++ b/tests/auth.refresh.api.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { server, http, HttpResponse } from './test-setup' +import { currentUser, logout } from '@/api/auth' +import { api } from '@/api/client' + +describe('auth refresh (currentUser)', () => { + beforeEach(async () => { + await logout() + }) + it('currentUser refresh returns user and sets auth token', async () => { + server.use( + http.post('*/api/v1/auth/refresh', () => + HttpResponse.json({ accessToken: 'ref-123', tokenType: 'bearer', user: { id: 9, email: 'ref@example.com', displayName: 'Refreshed' } }) + ) + ) + const user = await currentUser() + expect(user?.id).toBe(9) + expect(user?.displayName).toBe('Refreshed') + + // Subsequent API call should include Authorization header + server.use( + http.get('*/api/v1/users/me/households', ({ request }) => { + const auth = request.headers.get('authorization') + expect(auth?.toLowerCase()).toBe('bearer ref-123') + return HttpResponse.json([]) + }) + ) + const res = await api.GET('/api/v1/users/me/households', { params: {} }) + expect(res.response.ok).toBe(true) + }) + + it('returns null if refresh fails', async () => { + server.use(http.post('*/api/v1/auth/refresh', () => HttpResponse.json({ message: 'not logged in' }, { status: 401 }))) + const user = await currentUser() + expect(user).toBeNull() + server.use( + http.get('*/api/v1/users/me/households', ({ request }) => { + const auth = request.headers.get('authorization') + expect(auth).toBeFalsy() + return HttpResponse.json([]) + }) + ) + const res = await api.GET('/api/v1/users/me/households', { params: {} }) + expect(res.response.ok).toBe(true) + }) +})