haixunMaster/app/api/threads/oauth/callback/route.ts

117 lines
3.7 KiB
TypeScript

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);
}
}