Implement handleGoogleLogin (returns backend OAuth start URL)
This commit is contained in:
parent
d7c9788f00
commit
5586e2eede
4 changed files with 77 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
39
tests/auth.google.api.test.ts
Normal file
39
tests/auth.google.api.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue