moonlantern1's picture
Use public origin for Google OAuth
1c40bf6
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));
}
}