import { NextResponse, NextRequest } from "next/server"; import { db } from "@/lib/db"; import { accounts } from "@/lib/db/schema"; import { eq, and } from "drizzle-orm"; import crypto from "crypto"; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const code = searchParams.get("code"); const userId = searchParams.get("state"); const error = searchParams.get("error"); const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; if (error) { return NextResponse.redirect(new URL(`/dashboard?error=${encodeURIComponent(error)}`, appUrl)); } if (!code || !userId) { return NextResponse.redirect(new URL("/dashboard?error=Missing+code+or+state", appUrl)); } try { const clientId = process.env.GOOGLE_CLIENT_ID!; const clientSecret = process.env.GOOGLE_CLIENT_SECRET!; const redirectUri = `${appUrl}/api/youtube/callback`; 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: clientId, client_secret: clientSecret, redirect_uri: redirectUri, grant_type: "authorization_code", }).toString(), }); const tokens = await tokenResponse.json(); console.log("[YouTube callback] Tokens received:", { has_access_token: !!tokens.access_token, has_refresh_token: !!tokens.refresh_token, expires_in: tokens.expires_in, scope: tokens.scope, }); if (!tokens.access_token) { console.error("[YouTube callback] Token exchange failed:", tokens); throw new Error(`Token exchange failed: ${tokens.error_description || tokens.error || JSON.stringify(tokens)}`); } // Extract Google account ID from id_token (JWT sub claim) let googleAccountId = "google"; if (tokens.id_token) { try { const payload = JSON.parse( Buffer.from(tokens.id_token.split(".")[1], "base64").toString() ); googleAccountId = payload.sub; } catch { // Fall back to generic accountId } } const now = new Date(); const tokenFields = { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || null, scope: tokens.scope || null, accessTokenExpiresAt: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000) : null, }; // Upsert: update if the account row exists, insert if a prior disconnect deleted it const existing = await db.query.accounts.findFirst({ where: and(eq(accounts.userId, userId), eq(accounts.providerId, "google")), }); if (existing) { console.log("[YouTube callback] Updating existing account record for user:", userId); await db .update(accounts) .set({ ...tokenFields, updatedAt: now }) .where(eq(accounts.id, existing.id)); } else { console.log("[YouTube callback] Creating new account record for user:", userId); await db.insert(accounts).values({ id: crypto.randomUUID(), accountId: googleAccountId, providerId: "google", userId, ...tokenFields, createdAt: now, updatedAt: now, }); } // Redirect back to dashboard to trigger channel sync return NextResponse.redirect(new URL("/dashboard?sync_youtube=true", appUrl)); } catch (err) { console.error("[YouTube callback] Error:", err); return NextResponse.redirect( new URL(`/dashboard?error=${encodeURIComponent("Failed to complete YouTube connection")}`, appUrl) ); } }