117 lines
3.7 KiB
TypeScript
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);
|
|
}
|
|
}
|