import { NextResponse } from "next/server"; import { getProvider, generateAuthData, exchangeTokens, requestDeviceCode, pollForToken } from "@/lib/oauth/providers"; import { createProviderConnection } from "@/models"; /** * Dynamic OAuth API Route * Handles: authorize, exchange, device-code, poll */ // GET /api/oauth/[provider]/authorize - Generate auth URL // GET /api/oauth/[provider]/device-code - Request device code (for device_code flow) export async function GET(request, { params }) { try { const { provider, action } = await params; const { searchParams } = new URL(request.url); if (action === "authorize") { const redirectUri = searchParams.get("redirect_uri") || "http://localhost:8080/callback"; const authData = generateAuthData(provider, redirectUri); return NextResponse.json(authData); } if (action === "device-code") { const providerData = getProvider(provider); if (providerData.flowType !== "device_code") { return NextResponse.json({ error: "Provider does not support device code flow" }, { status: 400 }); } const authData = generateAuthData(provider, null); // Providers that don't use PKCE for device code const noPkceDeviceProviders = ["github", "kiro", "kimi-coding", "kilocode"]; let deviceData; if (noPkceDeviceProviders.includes(provider)) { deviceData = await requestDeviceCode(provider); } else { // Qwen and other PKCE providers deviceData = await requestDeviceCode(provider, authData.codeChallenge); } return NextResponse.json({ ...deviceData, codeVerifier: authData.codeVerifier, }); } return NextResponse.json({ error: "Unknown action" }, { status: 400 }); } catch (error) { console.log("OAuth GET error:", error); return NextResponse.json({ error: error.message }, { status: 500 }); } } // POST /api/oauth/[provider]/exchange - Exchange code for tokens and save // POST /api/oauth/[provider]/poll - Poll for token (device_code flow) export async function POST(request, { params }) { try { const { provider, action } = await params; let body; try { body = await request.json(); } catch { return NextResponse.json({ error: "Invalid or empty request body" }, { status: 400 }); } if (action === "exchange") { const { code, redirectUri, codeVerifier, state } = body; // Cline uses authorization_code without PKCE const noPkceExchangeProviders = ["cline"]; if (!code || !redirectUri || (!codeVerifier && !noPkceExchangeProviders.includes(provider))) { return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); } // Exchange code for tokens const tokenData = await exchangeTokens(provider, code, redirectUri, codeVerifier, state); // Save to database const connection = await createProviderConnection({ provider, authType: "oauth", ...tokenData, expiresAt: tokenData.expiresIn ? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString() : null, testStatus: "active", }); return NextResponse.json({ success: true, connection: { id: connection.id, provider: connection.provider, email: connection.email, displayName: connection.displayName, } }); } if (action === "poll") { const { deviceCode, codeVerifier, extraData } = body; if (!deviceCode) { return NextResponse.json({ error: "Missing device code" }, { status: 400 }); } // Providers that don't use PKCE for device code const noPkceProviders = ["github", "kimi-coding", "kilocode"]; let result; if (noPkceProviders.includes(provider)) { result = await pollForToken(provider, deviceCode); } else if (provider === "kiro") { // Kiro needs extraData (clientId, clientSecret) from device code response result = await pollForToken(provider, deviceCode, null, extraData); } else { // Qwen and other PKCE providers if (!codeVerifier) { return NextResponse.json({ error: "Missing code verifier" }, { status: 400 }); } result = await pollForToken(provider, deviceCode, codeVerifier); } if (result.success) { // Save to database const connection = await createProviderConnection({ provider, authType: "oauth", ...result.tokens, expiresAt: result.tokens.expiresIn ? new Date(Date.now() + result.tokens.expiresIn * 1000).toISOString() : null, testStatus: "active", }); return NextResponse.json({ success: true, connection: { id: connection.id, provider: connection.provider, } }); } // Still pending or error - don't create connection for pending states const isPending = result.pending || result.error === "authorization_pending" || result.error === "slow_down"; return NextResponse.json({ success: false, error: result.error, errorDescription: result.errorDescription, pending: isPending, }); } return NextResponse.json({ error: "Unknown action" }, { status: 400 }); } catch (error) { console.log("OAuth POST error:", error); return NextResponse.json({ error: error.message }, { status: 500 }); } }