| import { NextRequest, NextResponse } from 'next/server'; |
| import { |
| createSessionValue, |
| getAuthPublicOrigin, |
| OAUTH_STATE_COOKIE, |
| parseOAuthState, |
| SESSION_COOKIE, |
| sessionCookieOptions, |
| } from '@/lib/server/foodstarAuth'; |
| import { saveReviewForUser } from '@/lib/server/userReviewStore'; |
| import { FOODSTAR_JOURNAL_SLUG } from '@/lib/foodstar'; |
|
|
| export const dynamic = 'force-dynamic'; |
| export const runtime = 'nodejs'; |
|
|
| type GoogleUserInfo = { |
| sub?: string; |
| email?: string; |
| name?: string; |
| picture?: string; |
| }; |
|
|
| export async function GET(req: NextRequest) { |
| const clientId = process.env.GOOGLE_CLIENT_ID; |
| const clientSecret = process.env.GOOGLE_CLIENT_SECRET; |
| const url = new URL(req.url); |
| const publicOrigin = getAuthPublicOrigin(req, url); |
| const code = url.searchParams.get('code'); |
| const stateParam = url.searchParams.get('state'); |
| const stateCookie = req.cookies.get(OAUTH_STATE_COOKIE)?.value; |
| const state = stateParam && stateParam === stateCookie ? parseOAuthState(stateParam) : null; |
|
|
| if (!clientId || !clientSecret || !code || !state) { |
| return NextResponse.redirect(new URL('/me?auth=failed', publicOrigin)); |
| } |
|
|
| try { |
| const tokenRes = await fetch('https://oauth2.googleapis.com/token', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, |
| body: new URLSearchParams({ |
| code, |
| client_id: clientId, |
| client_secret: clientSecret, |
| redirect_uri: `${publicOrigin}/api/auth/google/callback`, |
| grant_type: 'authorization_code', |
| }), |
| cache: 'no-store', |
| }); |
|
|
| if (!tokenRes.ok) throw new Error('Token exchange failed'); |
| const tokenBody = (await tokenRes.json()) as { access_token?: string }; |
| if (!tokenBody.access_token) throw new Error('Missing Google access token'); |
|
|
| const userRes = await fetch('https://openidconnect.googleapis.com/v1/userinfo', { |
| headers: { Authorization: `Bearer ${tokenBody.access_token}` }, |
| cache: 'no-store', |
| }); |
| if (!userRes.ok) throw new Error('Google profile fetch failed'); |
|
|
| const googleUser = (await userRes.json()) as GoogleUserInfo; |
| if (!googleUser.sub || !googleUser.email) throw new Error('Google profile missing email'); |
|
|
| if (state.submissionId) { |
| saveReviewForUser({ |
| userEmail: googleUser.email, |
| submissionId: state.submissionId, |
| campaignSlug: state.campaignSlug || FOODSTAR_JOURNAL_SLUG, |
| placeName: state.placeName, |
| dishName: state.dishName, |
| }); |
| } |
|
|
| const res = NextResponse.redirect(new URL(state.returnTo || '/me', publicOrigin)); |
| res.cookies.set( |
| SESSION_COOKIE, |
| createSessionValue({ |
| sub: googleUser.sub, |
| email: googleUser.email, |
| name: googleUser.name || googleUser.email, |
| picture: googleUser.picture, |
| }), |
| sessionCookieOptions(), |
| ); |
| res.cookies.set(OAUTH_STATE_COOKIE, '', { |
| ...sessionCookieOptions(), |
| maxAge: 0, |
| }); |
| return res; |
| } catch { |
| return NextResponse.redirect(new URL('/me?auth=failed', publicOrigin)); |
| } |
| } |
|
|