Spaces:
Runtime error
Runtime error
File size: 4,166 Bytes
ceb943f 081358f ceb943f 081358f ceb943f 081358f ceb943f 081358f ceb943f 081358f ceb943f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | 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)
);
}
}
|