diff --git a/api/auth/foundation-callback.ts b/api/auth/foundation-callback.ts new file mode 100644 index 00000000..5616f4e3 --- /dev/null +++ b/api/auth/foundation-callback.ts @@ -0,0 +1,144 @@ +/** + * Foundation OAuth Callback Handler + * + * This endpoint receives the authorization code from aethex.foundation after user authentication. + * It exchanges the code for an access token and establishes a session on aethex.dev. + */ + +import { getAdminClient } from "../_supabase"; +import { VercelRequest, VercelResponse } from "@vercel/node"; + +const FOUNDATION_URL = process.env.VITE_FOUNDATION_URL || "https://aethex.foundation"; +const API_BASE = process.env.VITE_API_BASE || "https://aethex.dev"; + +interface FoundationTokenResponse { + access_token: string; + token_type: string; + expires_in: number; + user: { + id: string; + email: string; + username: string; + profile_complete: boolean; + }; +} + +export default async function handler( + req: VercelRequest, + res: VercelResponse, +) { + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { code, state, error } = req.query; + + // Handle Foundation errors + if (error) { + const errorDesc = req.query.error_description || error; + return res.redirect(`/login?error=${error}&message=${encodeURIComponent(String(errorDesc))}`); + } + + if (!code) { + return res.redirect("/login?error=no_code&message=Authorization code not received"); + } + + try { + // Parse state to get redirect destination + let redirectTo = "/dashboard"; + if (state) { + try { + const stateData = JSON.parse(decodeURIComponent(String(state))); + redirectTo = stateData.redirectTo || redirectTo; + } catch { + // Ignore state parsing errors, use default + } + } + + // Exchange code for token with Foundation + const tokenResponse = await fetch(`${FOUNDATION_URL}/api/oauth/token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + grant_type: "authorization_code", + code, + client_id: "aethex-corp", + client_secret: process.env.FOUNDATION_OAUTH_CLIENT_SECRET, + redirect_uri: `${API_BASE}/api/auth/foundation-callback`, + }), + }); + + if (!tokenResponse.ok) { + const errorData = await tokenResponse.json().catch(() => ({})); + console.error("[Foundation OAuth] Token exchange failed:", errorData); + return res.redirect( + `/login?error=token_exchange&message=${encodeURIComponent("Failed to exchange authorization code")}` + ); + } + + const tokenData = (await tokenResponse.json()) as FoundationTokenResponse; + + if (!tokenData.access_token || !tokenData.user) { + console.error("[Foundation OAuth] Invalid token response"); + return res.redirect("/login?error=invalid_token&message=Invalid token response"); + } + + // Extract user information from Foundation response + const { user, access_token } = tokenData; + + // Ensure user profile exists in aethex.dev's local database + // This syncs the user profile from Foundation + const supabase = getAdminClient(); + + // Get or create user profile locally + const { data: existingProfile, error: fetchError } = await supabase + .from("user_profiles") + .select("id") + .eq("id", user.id) + .single(); + + if (fetchError && fetchError.code !== "PGRST116") { + // PGRST116 = no rows found (expected for new users) + console.error("[Foundation OAuth] Error fetching user profile:", fetchError); + } + + if (!existingProfile) { + // Create user profile from Foundation data + const { error: createError } = await supabase.from("user_profiles").insert({ + id: user.id, + email: user.email, + username: user.username || null, + full_name: user.full_name || null, + profile_completed: user.profile_complete || false, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }); + + if (createError) { + console.error("[Foundation OAuth] Failed to create user profile:", createError); + return res.redirect( + `/login?error=profile_create&message=${encodeURIComponent("Failed to create local user profile")}` + ); + } + } + + // Set session cookie with Foundation's access token + // The frontend will use this token for authenticated requests + res.setHeader("Set-Cookie", [ + `foundation_access_token=${access_token}; Path=/; HttpOnly; Secure; SameSite=Strict`, + `auth_user_id=${user.id}; Path=/; Secure; SameSite=Strict`, + ]); + + console.log("[Foundation OAuth] Successfully authenticated user:", user.id); + + // Redirect to intended destination or dashboard + return res.redirect(redirectTo); + } catch (error) { + console.error("[Foundation OAuth] Callback error:", error); + return res.redirect( + `/login?error=unknown&message=${encodeURIComponent("An unexpected error occurred")}` + ); + } +}