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)); } }