import { NextResponse } from "next/server"; import { prisma } from "@/lib/db"; import { getOrCreateSettingsForUser } from "@/lib/user-settings"; import { getActiveAccountId } from "@/lib/account-context"; import { assertAccountOwnedByUser } from "@/lib/auth/accounts"; import { getSessionUser } from "@/lib/auth/session"; import { exchangeCodeForToken, exchangeForLongLivedToken, fetchThreadsProfile, getAppBaseUrl, getThreadsAppId, getThreadsAppSecret, saveThreadsAuthForAccount, } from "@/lib/threads-api"; export async function GET(request: Request) { let homeUrl: URL; try { homeUrl = new URL("/matrix", new URL(request.url).origin); } catch { return NextResponse.json({ error: "invalid request url" }, { status: 400 }); } try { const sessionUser = await getSessionUser(); const settings = sessionUser ? await getOrCreateSettingsForUser(sessionUser.id) : null; homeUrl = new URL("/matrix", getAppBaseUrl(request, settings?.appUrl)); const { searchParams } = new URL(request.url); const error = searchParams.get("error"); const code = searchParams.get("code"); const state = searchParams.get("state"); if (error) { homeUrl.searchParams.set("threads_error", error); return NextResponse.redirect(homeUrl); } if (!code) { homeUrl.searchParams.set("threads_error", "missing_code"); return NextResponse.redirect(homeUrl); } if (!sessionUser || !settings) { homeUrl.searchParams.set("threads_error", "請先登入後再綁定 Threads"); return NextResponse.redirect(homeUrl); } const appId = getThreadsAppId(); const appSecret = getThreadsAppSecret(); if (!appId || !appSecret) { homeUrl.searchParams.set("threads_error", "missing_app_credentials"); return NextResponse.redirect(homeUrl); } const stateAccountId = state && state !== "threadtools" ? state : null; const accountId = stateAccountId ?? (await getActiveAccountId()); if (!accountId) { homeUrl.searchParams.set("threads_error", "no_active_account"); return NextResponse.redirect(homeUrl); } let accountExists; try { accountExists = await assertAccountOwnedByUser(sessionUser.id, accountId); } catch { homeUrl.searchParams.set("threads_error", "account_not_found"); return NextResponse.redirect(homeUrl); } const redirectUri = `${getAppBaseUrl(request, settings.appUrl)}/api/threads/oauth/callback`; const shortLived = await exchangeCodeForToken({ appId, appSecret, code, redirectUri, }); const longLived = await exchangeForLongLivedToken(appSecret, shortLived.accessToken); await saveThreadsAuthForAccount(accountId, { accessToken: longLived.accessToken, userId: shortLived.userId, expiresIn: longLived.expiresIn, }); const profile = await fetchThreadsProfile(longLived.accessToken); if (profile.username) { const isPlaceholderName = !accountExists.displayName || accountExists.displayName === "待綁定帳號" || accountExists.displayName === "新經營帳號"; await prisma.account.update({ where: { id: accountId }, data: { username: profile.username, valid: true, ...(isPlaceholderName ? { displayName: `@${profile.username}` } : {}), }, }); } await prisma.setting.update({ where: { userId: sessionUser.id }, data: { publishViaApi: true }, }); homeUrl.searchParams.set("threads_connected", "1"); return NextResponse.redirect(homeUrl); } catch (err) { const message = err instanceof Error ? err.message : "oauth_failed"; homeUrl.searchParams.set("threads_error", message); return NextResponse.redirect(homeUrl); } }