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