File size: 3,100 Bytes
d99b1af 1c40bf6 d99b1af 1c40bf6 d99b1af 1c40bf6 d99b1af 1c40bf6 d99b1af 1c40bf6 d99b1af 1c40bf6 d99b1af | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | 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));
}
}
|