import { createClient } from "@supabase/supabase-js"; import { notifyAccountLinked } from "../../_notifications.js"; import { getAdminClient } from "../../_supabase.js"; import { federateOAuthUser, linkProviderToPassport, } from "../../_oauth-federation.js"; export const config = { runtime: "nodejs", }; interface GoogleTokenResponse { access_token: string; token_type: string; expires_in: number; id_token: string; } interface GoogleUserInfo { sub: string; email: string; name: string; picture?: string; } function decodeJWT(token: string): any { try { const parts = token.split("."); if (parts.length !== 3) return null; const decoded = Buffer.from(parts[1], "base64").toString("utf-8"); return JSON.parse(decoded); } catch (e) { return null; } } export default async function handler(req: any, res: any) { if (req.method !== "GET") { return res.status(405).json({ error: "Method not allowed" }); } const { code, state, error } = req.query; // Handle Google error if (error) { return res.redirect(`/login?error=${error}`); } if (!code) { return res.redirect("/login?error=no_code"); } // Parse state to determine if this is a linking or login flow let isLinkingFlow = false; let redirectTo = "/dashboard"; if (state) { try { const stateData = JSON.parse( Buffer.from(state as string, "base64").toString("utf-8"), ); isLinkingFlow = stateData.action === "link"; redirectTo = stateData.redirectTo || redirectTo; } catch (e) { console.log("[Google OAuth] Could not parse state:", e); } } // For linking flow, extract user ID from temporary session stored in database let authenticatedUserId: string | null = null; if (isLinkingFlow) { try { const stateData = JSON.parse( Buffer.from(state as string, "base64").toString("utf-8"), ); const sessionToken = stateData.sessionToken; if (!sessionToken) { console.error( "[Google OAuth] No session token found in linking flow state", ); return res.redirect( "/login?error=session_lost&message=Session%20expired.%20Please%20try%20linking%20Google%20again.", ); } // Query database for the temporary linking session const tempAdminClient = getAdminClient(); const { data: session, error: sessionError } = await tempAdminClient .from("discord_linking_sessions") .select("user_id") .eq("session_token", sessionToken) .gt("expires_at", new Date().toISOString()) .single(); if (sessionError || !session) { console.error( "[Google OAuth] Linking session not found or expired", sessionError, ); return res.redirect( "/login?error=session_lost&message=Session%20expired.%20Please%20try%20linking%20Google%20again.", ); } authenticatedUserId = session.user_id; console.log( "[Google OAuth] Linking session found, user_id:", authenticatedUserId, ); // Clean up: delete the temporary session await tempAdminClient .from("discord_linking_sessions") .delete() .eq("session_token", sessionToken); } catch (e) { console.error("[Google OAuth] Error parsing/using session token:", e); return res.redirect( "/login?error=session_lost&message=Session%20error.%20Please%20try%20again.", ); } } const clientId = process.env.GOOGLE_OAUTH_CLIENT_ID; const clientSecret = process.env.GOOGLE_OAUTH_CLIENT_SECRET; const supabaseUrl = process.env.VITE_SUPABASE_URL; const supabaseServiceRole = process.env.SUPABASE_SERVICE_ROLE; if (!clientId || !clientSecret || !supabaseUrl || !supabaseServiceRole) { console.error("[Google OAuth] Missing environment variables"); return res.redirect("/login?error=config"); } try { const apiBase = process.env.VITE_API_BASE || "https://aethex.dev"; const redirectUri = `${apiBase}/api/google/oauth/callback`; // Exchange code for access token const tokenResponse = await fetch("https://oauth2.googleapis.com/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, grant_type: "authorization_code", code, redirect_uri: redirectUri, }).toString(), }); if (!tokenResponse.ok) { const errorData = await tokenResponse.json(); console.error("[Google OAuth] Token exchange failed:", errorData); return res.redirect("/login?error=token_exchange"); } const tokenData = (await tokenResponse.json()) as GoogleTokenResponse; if (!tokenData.access_token) { console.error("[Google OAuth] No access token in response"); return res.redirect("/login?error=no_token"); } // Decode ID token to get user info let googleUser: GoogleUserInfo | null = null; if (tokenData.id_token) { googleUser = decodeJWT(tokenData.id_token) as GoogleUserInfo; } // Fallback: Fetch user info from Google API if JWT decode failed if (!googleUser) { const userResponse = await fetch( "https://www.googleapis.com/oauth2/v2/userinfo", { headers: { Authorization: `Bearer ${tokenData.access_token}`, }, }, ); if (!userResponse.ok) { console.error("[Google OAuth] User fetch failed:", userResponse.status); return res.redirect("/login?error=user_fetch"); } googleUser = (await userResponse.json()) as GoogleUserInfo; } if (!googleUser || !googleUser.email) { console.error("[Google OAuth] No email in user info"); return res.redirect( "/login?error=no_email&message=Please+enable+email+on+your+Google+account", ); } const supabase = getAdminClient(); // LINKING FLOW: Link Google to authenticated user's Foundation Passport if (isLinkingFlow && authenticatedUserId) { console.log( "[Google OAuth] Linking Google to user:", authenticatedUserId, ); try { await linkProviderToPassport(authenticatedUserId, "google", { id: googleUser.sub, email: googleUser.email, name: googleUser.name || undefined, avatar: googleUser.picture || undefined, }); console.log( "[Google OAuth] Successfully linked Google to user:", authenticatedUserId, ); await notifyAccountLinked(authenticatedUserId, "Google"); return res.redirect(redirectTo); } catch (linkError: any) { console.error("[Google OAuth] Linking failed:", linkError); return res.redirect( `/dashboard?error=link_failed&message=${encodeURIComponent(linkError?.message || "Failed to link Google account")}`, ); } } // LOGIN FLOW: OAuth Federation // Federate Google OAuth to Foundation Passport try { const federationResult = await federateOAuthUser("google", { id: googleUser.sub, email: googleUser.email, name: googleUser.name || undefined, avatar: googleUser.picture || undefined, }); console.log("[Google OAuth] Federation result:", { user_id: federationResult.user_id, is_new_user: federationResult.is_new_user, provider_linked: federationResult.provider_linked, }); // Send notification if this is a new user if (federationResult.is_new_user) { await notifyAccountLinked(federationResult.user_id, "Google"); } // Redirect to dashboard after successful federation return res.redirect("/dashboard"); } catch (federationError) { console.error("[Google OAuth] Federation error:", federationError); return res.redirect( `/login?error=federation_failed&message=${encodeURIComponent("Failed to link Google account. Please try again.")}`, ); } } catch (error) { console.error("[Google OAuth] Callback error:", error); res.redirect("/login?error=unknown"); } }