dvijaykrishnan's picture
feat: Introduce a utility to correct YouTube channel types, add a full video sync trigger, and refine dashboard channel management.
081358f
Raw
History Blame Contribute Delete
4.17 kB
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)
);
}
}