import { NextRequest, NextResponse } from "next/server"; import { cookies } from "next/headers"; import { type Provider, exchangeCode, findOrCreateOAuthUser, verifyState, } from "@/lib/oauth"; import { signJWT } from "@/lib/auth"; const VALID_PROVIDERS = ["google", "github"]; async function handleCallback( req: NextRequest, provider: string ): Promise { try { // Grab code and state from query params or POST body const url = new URL(req.url); let code = url.searchParams.get("code"); let stateParam = url.searchParams.get("state"); // Try reading POST body if code wasn't in query params if (!code) { try { const contentType = req.headers.get("content-type") || ""; if (contentType.includes("form")) { const formData = await req.formData(); code = formData.get("code") as string | null; stateParam = formData.get("state") as string | null; } } catch { // not a form POST, that's fine } } if (!code) { return NextResponse.json( { error: "Missing authorization code" }, { status: 400 } ); } if (!stateParam) { return NextResponse.json( { error: "Missing state parameter" }, { status: 400 } ); } // Verify state from cookie (CSRF protection) const cookieStore = await cookies(); const storedState = cookieStore.get("oauth_state")?.value; if (!storedState) { return NextResponse.json( { error: "Missing state cookie. Please start the OAuth flow again.", }, { status: 400 } ); } const statePayload = await verifyState(stateParam); if (!statePayload) { return NextResponse.json( { error: "Invalid or expired state. Please start the OAuth flow again.", }, { status: 400 } ); } // Verify the state and provider match if (statePayload.provider !== provider) { return NextResponse.json( { error: "State/provider mismatch" }, { status: 400 } ); } // Clear the state cookie cookieStore.set("oauth_state", "", { httpOnly: true, path: "/", maxAge: 0, }); // Build the redirect URI (must match the one used in the authorization request) const forwardedProto = req.headers.get("x-forwarded-proto") || "https"; const rawHost = req.headers.get("host") || ""; const isDockerHost = /^[a-f0-9]{12,}$/i.test(rawHost.split(":")[0]); const forwardedHost = req.headers.get("x-forwarded-host") || (isDockerHost ? "falahos.my" : rawHost); const baseUrl = `${forwardedProto}://${forwardedHost}`; const redirectUri = `${baseUrl}/mobile/api/auth/${provider}/callback`; // Exchange code for user info const providerUser = await exchangeCode( provider as Provider, code, redirectUri ); // Find or create user in database const user = await findOrCreateOAuthUser( provider as Provider, providerUser ); // Generate JWT const token = await signJWT({ userId: user.id, email: user.email, isPremium: user.isPremium, isPro: user.isPro, }); // Redirect to frontend callback page with token const frontendUrl = `${baseUrl}/mobile/auth/callback?token=${encodeURIComponent(token)}`; return NextResponse.redirect(frontendUrl); } catch (error) { console.error(`${provider} OAuth callback error:`, error); const message = error instanceof Error ? error.message : "OAuth callback failed"; return NextResponse.json({ error: message }, { status: 500 }); } } export async function GET( req: NextRequest, { params }: { params: Promise<{ provider: string }> } ) { const { provider } = await params; if (!VALID_PROVIDERS.includes(provider)) { return NextResponse.json( { error: `Unsupported provider. Must be one of: ${VALID_PROVIDERS.join(", ")}`, }, { status: 400 } ); } return handleCallback(req, provider); }