Implement handleGoogleLogin (returns backend OAuth start URL)

This commit is contained in:
jableader 2025-11-01 22:14:36 +11:00
parent d7c9788f00
commit 5586e2eede
4 changed files with 77 additions and 6 deletions

View file

@ -179,6 +179,12 @@ Refinements (Nov 1, 2025, later):
- Raw parse endpoints now use small runtime guards and normalize to strict RecipeOut before decoding.
- UI polish: MemberList and EditMealPage CSS class names unified (person-* → member-*). Login page shows a Google sign-in button wired to the placeholder handler.
Google OAuth (Nov 1, 2025, later):
- Added tests for Google OAuth start and callback.
- Implemented `handleGoogleLogin()` to retrieve an OAuth start URL and navigate.
- Implemented `completeGoogleLogin(code, state?)` using raw fetch to exchange the code and set the token/user; verified Authorization header on subsequent calls.
- Current UI: Login page button uses the returned URL to redirect. Callback route/UI still pending wiring (next).
---
## Detailed Tasks by File/Module

View file

@ -1,4 +1,4 @@
import { api } from '@/api/client'
import { api, fetchApi } from '@/api/client'
import { setAuthTokenProvider } from '@/api/client'
import type { User } from '@/domain/types'
@ -69,9 +69,33 @@ export async function createAccount(email: string, displayName: string, password
return cachedUser
}
export async function handleGoogleLogin(): Promise<never> {
// Placeholder until Google OAuth flow is wired
throw new Error('handleGoogleLogin not implemented yet')
// Google OAuth
export async function handleGoogleLogin(): Promise<string> {
// Ask backend for the Google OAuth start URL
const res = await fetchApi('/api/v1/auth/google/start', { method: 'GET' })
if (!res.ok) throw new Error(`${res.status} ${res.statusText || 'HTTP error'}`)
const data = (await res.json()) as unknown
const url = typeof (data as any)?.url === 'string' ? (data as any).url : null
if (!url) throw new Error('Invalid google start response')
return url
}
export async function completeGoogleLogin(code: string, state?: string): Promise<User> {
const body: Record<string, unknown> = { code }
if (state) body.state = state
const res = await fetchApi('/api/v1/auth/google/callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (!res.ok) throw new Error(`${res.status} ${res.statusText || 'HTTP error'}`)
const data: unknown = await res.json()
const token = (data && typeof (data as any).accessToken === 'string') ? (data as any).accessToken as string : null
const user = (data && (data as any).user && typeof (data as any).user.id === 'number') ? (data as any).user as { id: number; email?: string; displayName?: string } : null
if (!token || !user) throw new Error('Invalid token response')
authToken = token
cachedUser = { id: user.id, email: user.email ?? '', displayName: user.displayName ?? '' }
return cachedUser
}
export async function logout(): Promise<void> {

View file

@ -91,9 +91,11 @@ async function onSubmitLogin() {
async function onGoogleLogin() {
try {
await handleGoogleLogin()
const url = await handleGoogleLogin()
if (typeof window !== 'undefined') {
window.location.href = url
}
} catch (e) {
// Surface the placeholder message for now; real implementation will redirect
alert(e instanceof Error ? e.message : 'Google login not available')
}
}

View file

@ -0,0 +1,39 @@
import { describe, it, expect } from 'vitest'
import { server, http, HttpResponse } from './test-setup'
import { handleGoogleLogin, completeGoogleLogin } from '@/api/auth'
import { api } from '@/api/client'
describe('auth google oauth', () => {
it('handleGoogleLogin returns start URL', async () => {
server.use(
http.get('*/api/v1/auth/google/start', () => {
return HttpResponse.json({ url: 'https://accounts.google.com/o/oauth2/v2/auth?x=y' })
})
)
const url = await handleGoogleLogin()
expect(url).toContain('https://accounts.google.com')
})
it('completeGoogleLogin exchanges code for token and sets auth header', async () => {
server.use(
http.post('*/api/v1/auth/google/callback', async ({ request }) => {
const body = await request.json()
expect(body).toEqual({ code: 'abc', state: 'state-1' })
return HttpResponse.json({ accessToken: 'goog-123', tokenType: 'bearer', user: { id: 42, email: 'g@example.com', displayName: 'G User' } })
})
)
const user = await completeGoogleLogin('abc', 'state-1')
expect(user.id).toBe(42)
// subsequent requests include bearer
server.use(
http.get('*/api/v1/users/me/households', ({ request }) => {
const auth = request.headers.get('authorization')
expect(auth?.toLowerCase()).toBe('bearer goog-123')
return HttpResponse.json([])
})
)
const res = await api.GET('/api/v1/users/me/households', { params: {} })
expect(res.response.ok).toBe(true)
})
})