import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { getAuthCookieOptions } from '@/lib/cookieConfig'; import { appendRedirectParam, GOOGLE_OAUTH_STATE_COOKIE_NAME, POST_AUTH_REDIRECT_COOKIE_NAME, sanitizeRedirectPath, } from '@/lib/auth-flow'; import { ATTRIBUTION_COOKIE_NAME, getEmailDomain, parseAttributionCookie, shouldResumeOnboarding, } from '@/lib/revops'; import { triggerLifecycleScoring } from '@/lib/revops-server'; const isProduction = process.env.NODE_ENV === 'production'; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const code = searchParams.get('code'); const state = searchParams.get('state'); const firstTouch = parseAttributionCookie(request.cookies.get(ATTRIBUTION_COOKIE_NAME)?.value); const savedOauthState = request.cookies.get(GOOGLE_OAUTH_STATE_COOKIE_NAME)?.value; const savedRedirect = sanitizeRedirectPath(request.cookies.get(POST_AUTH_REDIRECT_COOKIE_NAME)?.value); // If no code, redirect to Google OAuth if (!code) { const googleClientId = process.env.GOOGLE_CLIENT_ID; if (!googleClientId) { return NextResponse.json( { error: 'Google Client ID not configured' }, { status: 500 } ); } const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/google`; const scope = 'openid email profile'; const redirectTarget = sanitizeRedirectPath(searchParams.get('redirect')); const oauthState = crypto.randomUUID(); const googleAuthUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth'); googleAuthUrl.searchParams.set('client_id', googleClientId); googleAuthUrl.searchParams.set('redirect_uri', redirectUri); googleAuthUrl.searchParams.set('response_type', 'code'); googleAuthUrl.searchParams.set('scope', scope); googleAuthUrl.searchParams.set('state', oauthState); const response = NextResponse.redirect(googleAuthUrl); response.cookies.set(GOOGLE_OAUTH_STATE_COOKIE_NAME, oauthState, { httpOnly: true, secure: isProduction, sameSite: 'lax', path: '/', maxAge: 60 * 10, }); if (redirectTarget) { response.cookies.set(POST_AUTH_REDIRECT_COOKIE_NAME, redirectTarget, { httpOnly: true, secure: isProduction, sameSite: 'lax', path: '/', maxAge: 60 * 10, }); } else { response.cookies.delete(POST_AUTH_REDIRECT_COOKIE_NAME); } return response; } // Handle callback with code try { if (!state || !savedOauthState || state !== savedOauthState) { const invalidStateResponse = NextResponse.redirect( `${process.env.NEXT_PUBLIC_APP_URL}/login?error=google-state-invalid` ); invalidStateResponse.cookies.delete(GOOGLE_OAUTH_STATE_COOKIE_NAME); invalidStateResponse.cookies.delete(POST_AUTH_REDIRECT_COOKIE_NAME); return invalidStateResponse; } const googleClientId = process.env.GOOGLE_CLIENT_ID; const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET; if (!googleClientId || !googleClientSecret) { return NextResponse.json( { error: 'Google OAuth not configured' }, { status: 500 } ); } const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/google`; // Exchange code for tokens const tokenResponse = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ code, client_id: googleClientId, client_secret: googleClientSecret, redirect_uri: redirectUri, grant_type: 'authorization_code', }), }); if (!tokenResponse.ok) { throw new Error('Failed to exchange code for tokens'); } const tokens = await tokenResponse.json(); // Get user info from Google const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { headers: { Authorization: `Bearer ${tokens.access_token}`, }, }); if (!userInfoResponse.ok) { throw new Error('Failed to get user info'); } const userInfo = await userInfoResponse.json(); // Check if user exists in database let user = await db.user.findUnique({ where: { email: userInfo.email }, }); const isNewUser = !user; // Create user if they don't exist if (!user) { const onboardingStartedAt = new Date(); user = await db.user.create({ data: { email: userInfo.email, name: userInfo.name || userInfo.email.split('@')[0], image: userInfo.picture, emailVerified: new Date(), // Google already verified the email password: null, // OAuth users don't need a password onboardingStartedAt, emailDomain: getEmailDomain(userInfo.email), signupSource: firstTouch?.signupSource || null, signupMedium: firstTouch?.signupMedium || null, signupCampaign: firstTouch?.signupCampaign || null, signupContent: firstTouch?.signupContent || null, signupTerm: firstTouch?.signupTerm || null, signupReferrer: firstTouch?.signupReferrer || null, signupLandingPath: firstTouch?.signupLandingPath || '/signup', signupFirstSeenAt: firstTouch?.signupFirstSeenAt ? new Date(firstTouch.signupFirstSeenAt) : onboardingStartedAt, }, }); // Create Account entry for the OAuth provider await db.account.create({ data: { userId: user.id, type: 'oauth', provider: 'google', providerAccountId: userInfo.sub || userInfo.id, access_token: tokens.access_token, refresh_token: tokens.refresh_token, expires_at: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : null, token_type: tokens.token_type, scope: tokens.scope, id_token: tokens.id_token, }, }); } else { // Update existing account tokens const existingAccount = await db.account.findUnique({ where: { provider_providerAccountId: { provider: 'google', providerAccountId: userInfo.sub || userInfo.id, }, }, }); if (existingAccount) { await db.account.update({ where: { id: existingAccount.id }, data: { access_token: tokens.access_token, refresh_token: tokens.refresh_token, expires_at: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : null, }, }); } else { // Create Account entry if it doesn't exist await db.account.create({ data: { userId: user.id, type: 'oauth', provider: 'google', providerAccountId: userInfo.sub || userInfo.id, access_token: tokens.access_token, refresh_token: tokens.refresh_token, expires_at: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : null, token_type: tokens.token_type, scope: tokens.scope, id_token: tokens.id_token, }, }); } } triggerLifecycleScoring(user.id, isNewUser ? 'signup' : 'subscription_changed'); const onboardingTarget = isNewUser || shouldResumeOnboarding(user) ? appendRedirectParam('/onboarding', savedRedirect, { authMethod: 'google', isNewUser: isNewUser.toString(), }) : (savedRedirect || appendRedirectParam('/dashboard', null, { authMethod: 'google', isNewUser: isNewUser.toString(), })); const redirectUrl = new URL(`${process.env.NEXT_PUBLIC_APP_URL}${onboardingTarget}`); const response = NextResponse.redirect(redirectUrl.toString()); response.cookies.set('userId', user.id, getAuthCookieOptions()); response.cookies.delete(GOOGLE_OAUTH_STATE_COOKIE_NAME); response.cookies.delete(POST_AUTH_REDIRECT_COOKIE_NAME); response.cookies.delete(ATTRIBUTION_COOKIE_NAME); return response; } catch (error) { console.error('Google OAuth error:', error); const errorResponse = NextResponse.redirect( `${process.env.NEXT_PUBLIC_APP_URL}/login?error=google-signin-failed` ); errorResponse.cookies.delete(GOOGLE_OAUTH_STATE_COOKIE_NAME); errorResponse.cookies.delete(POST_AUTH_REDIRECT_COOKIE_NAME); return errorResponse; } }