feat: add the tiktok integration
This commit is contained in:
@@ -32,6 +32,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
],
|
||||
callbacks: {
|
||||
async session({ session, token }) {
|
||||
if (token?.id && session.user) {
|
||||
(session.user as { id?: string }).id = token.id as string;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
async jwt({ token, user }) {
|
||||
|
||||
150
lib/tiktok.ts
Normal file
150
lib/tiktok.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
const TIKTOK_AUTH_URL = "https://www.tiktok.com/v2/auth/authorize";
|
||||
const TIKTOK_TOKEN_URL = "https://open.tiktokapis.com/v2/oauth/token/";
|
||||
const TIKTOK_USER_INFO_URL = "https://open.tiktokapis.com/v2/user/info/";
|
||||
|
||||
const CLIENT_KEY = process.env.TIKTOK_CLIENT_KEY!;
|
||||
const CLIENT_SECRET = process.env.TIKTOK_CLIENT_SECRET!;
|
||||
|
||||
function getRedirectUri(): string {
|
||||
return process.env.NODE_ENV === "production"
|
||||
? process.env.TIKTOK_REDIRECT_URI_PROD!
|
||||
: process.env.TIKTOK_REDIRECT_URI_DEV!;
|
||||
}
|
||||
|
||||
// ── PKCE helpers ──────────────────────────────────────────────
|
||||
export function generateCodeVerifier(): string {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
const array = new Uint8Array(64);
|
||||
crypto.getRandomValues(array);
|
||||
for (let i = 0; i < 64; i++) {
|
||||
result += charset[array[i] % charset.length];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function generateCodeChallenge(verifier: string): Promise<string> {
|
||||
const data = new TextEncoder().encode(verifier);
|
||||
const digest = await globalThis.crypto.subtle.digest("SHA-256", data);
|
||||
const bytes = Array.from(new Uint8Array(digest));
|
||||
return btoa(bytes.map((b) => String.fromCharCode(b)).join(""))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
}
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function getTikTokAuthUrl(state: string, codeChallenge: string): string {
|
||||
const params = new URLSearchParams({
|
||||
client_key: CLIENT_KEY,
|
||||
response_type: "code",
|
||||
scope: "user.info.basic,user.info.stats",
|
||||
redirect_uri: getRedirectUri(),
|
||||
state,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: "S256",
|
||||
});
|
||||
return `${TIKTOK_AUTH_URL}?${params.toString()}`;
|
||||
}
|
||||
|
||||
export interface TikTokTokenResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
open_id: string;
|
||||
expires_in: number;
|
||||
refresh_expires_in: number;
|
||||
token_type: string;
|
||||
scope: string;
|
||||
error?: string;
|
||||
error_description?: string;
|
||||
}
|
||||
|
||||
export async function exchangeCodeForTokens(code: string, codeVerifier: string): Promise<TikTokTokenResponse> {
|
||||
const body = new URLSearchParams({
|
||||
client_key: CLIENT_KEY,
|
||||
client_secret: CLIENT_SECRET,
|
||||
code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: getRedirectUri(),
|
||||
code_verifier: codeVerifier,
|
||||
});
|
||||
|
||||
const res = await fetch(TIKTOK_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: body.toString(),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok || data.error) {
|
||||
throw new Error(data.error_description || data.error || "TikTok token exchange failed");
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(refreshToken: string): Promise<TikTokTokenResponse> {
|
||||
const body = new URLSearchParams({
|
||||
client_key: CLIENT_KEY,
|
||||
client_secret: CLIENT_SECRET,
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
|
||||
const res = await fetch(TIKTOK_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: body.toString(),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok || data.error) {
|
||||
throw new Error(data.error_description || data.error || "TikTok token refresh failed");
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export interface TikTokUserStats {
|
||||
followers: number;
|
||||
likes: number;
|
||||
videoCount: number;
|
||||
username: string;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
export async function fetchUserStats(accessToken: string, openId: string): Promise<TikTokUserStats> {
|
||||
const fields = "follower_count,following_count,likes_count,video_count,display_name,avatar_url";
|
||||
const url = `${TIKTOK_USER_INFO_URL}?fields=${fields}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok || data.error?.code !== "ok") {
|
||||
throw new Error(data.error?.message || "TikTok user info fetch failed");
|
||||
}
|
||||
|
||||
const user = data.data?.user ?? {};
|
||||
|
||||
return {
|
||||
followers: user.follower_count ?? 0,
|
||||
likes: user.likes_count ?? 0,
|
||||
videoCount: user.video_count ?? 0,
|
||||
username: openId,
|
||||
displayName: user.display_name ?? "",
|
||||
avatarUrl: user.avatar_url ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user