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.
|
- 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.
|
- 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
|
## 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 { setAuthTokenProvider } from '@/api/client'
|
||||||
import type { User } from '@/domain/types'
|
import type { User } from '@/domain/types'
|
||||||
|
|
||||||
|
|
@ -69,9 +69,33 @@ export async function createAccount(email: string, displayName: string, password
|
||||||
return cachedUser
|
return cachedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleGoogleLogin(): Promise<never> {
|
// Google OAuth
|
||||||
// Placeholder until Google OAuth flow is wired
|
export async function handleGoogleLogin(): Promise<string> {
|
||||||
throw new Error('handleGoogleLogin not implemented yet')
|
// 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> {
|
export async function logout(): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -91,9 +91,11 @@ async function onSubmitLogin() {
|
||||||
|
|
||||||
async function onGoogleLogin() {
|
async function onGoogleLogin() {
|
||||||
try {
|
try {
|
||||||
await handleGoogleLogin()
|
const url = await handleGoogleLogin()
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.location.href = url
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Surface the placeholder message for now; real implementation will redirect
|
|
||||||
alert(e instanceof Error ? e.message : 'Google login not available')
|
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