Spaces:
Runtime error
Runtime error
feat: Introduce a utility to correct YouTube channel types, add a full video sync trigger, and refine dashboard channel management.
081358f | 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) | |
| ); | |
| } | |
| } | |