TikTok api
This commit is contained in:
92
src/app/(main)/api/tiktok/callback/route.ts
Normal file
92
src/app/(main)/api/tiktok/callback/route.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { TIKTOK_ACCOUNT_KEY, TIKTOK_OAUTH_STATE_COOKIE_NAME } from '@/lib/tiktok';
|
||||
|
||||
const textResponse = (body: string, status: number) => {
|
||||
const response = new NextResponse(body, {
|
||||
status,
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||
});
|
||||
response.cookies.delete(TIKTOK_OAUTH_STATE_COOKIE_NAME);
|
||||
return response;
|
||||
};
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const code = searchParams.get('code');
|
||||
const state = searchParams.get('state');
|
||||
const error = searchParams.get('error');
|
||||
const errorDescription = searchParams.get('error_description');
|
||||
const savedState = request.cookies.get(TIKTOK_OAUTH_STATE_COOKIE_NAME)?.value;
|
||||
|
||||
if (error) {
|
||||
return textResponse(`TikTok authorization failed: ${errorDescription || error}`, 400);
|
||||
}
|
||||
if (!code) {
|
||||
return textResponse('Missing authorization code.', 400);
|
||||
}
|
||||
if (!state || !savedState || state !== savedState) {
|
||||
return textResponse('Invalid OAuth state. Start over at /api/tiktok/connect.', 403);
|
||||
}
|
||||
|
||||
const clientKey = process.env.TIKTOK_CLIENT_KEY;
|
||||
const clientSecret = process.env.TIKTOK_CLIENT_SECRET;
|
||||
if (!clientKey || !clientSecret) {
|
||||
return textResponse('TikTok client credentials are not configured.', 500);
|
||||
}
|
||||
|
||||
const redirectUri =
|
||||
process.env.TIKTOK_REDIRECT_URI || `${process.env.NEXT_PUBLIC_APP_URL}/api/tiktok/callback`;
|
||||
|
||||
try {
|
||||
const tokenResponse = await fetch('https://open.tiktokapis.com/v2/oauth/token/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
client_key: clientKey,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: redirectUri,
|
||||
}),
|
||||
});
|
||||
|
||||
const tokens = await tokenResponse.json();
|
||||
if (!tokenResponse.ok || tokens.error) {
|
||||
throw new Error(tokens.error_description || tokens.error || 'TikTok token exchange failed');
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const accessTokenExpiresAt = new Date(now + Number(tokens.expires_in || 0) * 1000);
|
||||
const refreshTokenExpiresAt = tokens.refresh_expires_in
|
||||
? new Date(now + Number(tokens.refresh_expires_in) * 1000)
|
||||
: null;
|
||||
|
||||
await db.tiktokIntegration.upsert({
|
||||
where: { accountKey: TIKTOK_ACCOUNT_KEY },
|
||||
create: {
|
||||
accountKey: TIKTOK_ACCOUNT_KEY,
|
||||
openId: tokens.open_id,
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
scope: tokens.scope || null,
|
||||
accessTokenExpiresAt,
|
||||
refreshTokenExpiresAt,
|
||||
},
|
||||
update: {
|
||||
openId: tokens.open_id,
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
scope: tokens.scope || null,
|
||||
accessTokenExpiresAt,
|
||||
refreshTokenExpiresAt,
|
||||
},
|
||||
});
|
||||
|
||||
return textResponse('TikTok account connected. You can close this tab.', 200);
|
||||
} catch (err) {
|
||||
console.error('TikTok callback error:', err);
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return textResponse(`Failed to connect TikTok account: ${message}`, 502);
|
||||
}
|
||||
}
|
||||
41
src/app/(main)/api/tiktok/connect/route.ts
Normal file
41
src/app/(main)/api/tiktok/connect/route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { TIKTOK_OAUTH_STATE_COOKIE_NAME } from '@/lib/tiktok';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const adminKey = process.env.TIKTOK_ADMIN_KEY;
|
||||
if (adminKey) {
|
||||
const provided = request.nextUrl.searchParams.get('key');
|
||||
if (provided !== adminKey) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
const clientKey = process.env.TIKTOK_CLIENT_KEY;
|
||||
if (!clientKey) {
|
||||
return NextResponse.json({ error: 'TIKTOK_CLIENT_KEY not configured' }, { status: 500 });
|
||||
}
|
||||
|
||||
const redirectUri =
|
||||
process.env.TIKTOK_REDIRECT_URI || `${process.env.NEXT_PUBLIC_APP_URL}/api/tiktok/callback`;
|
||||
|
||||
const oauthState = crypto.randomUUID();
|
||||
|
||||
const authUrl = new URL('https://www.tiktok.com/v2/auth/authorize/');
|
||||
authUrl.searchParams.set('client_key', clientKey);
|
||||
authUrl.searchParams.set('scope', 'user.info.basic,video.publish');
|
||||
authUrl.searchParams.set('response_type', 'code');
|
||||
authUrl.searchParams.set('redirect_uri', redirectUri);
|
||||
authUrl.searchParams.set('state', oauthState);
|
||||
|
||||
const response = NextResponse.redirect(authUrl);
|
||||
response.cookies.set(TIKTOK_OAUTH_STATE_COOKIE_NAME, oauthState, {
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 60 * 10,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
41
src/app/(main)/api/tiktok/token/route.ts
Normal file
41
src/app/(main)/api/tiktok/token/route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getValidTiktokTokens } from '@/lib/tiktok';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const adminKey = process.env.TIKTOK_ADMIN_KEY;
|
||||
if (!adminKey) {
|
||||
// Unlike /connect, this endpoint hands out live credentials — never expose
|
||||
// it without a configured key.
|
||||
return NextResponse.json(
|
||||
{ error: 'TIKTOK_ADMIN_KEY must be configured to expose tokens' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const provided =
|
||||
request.headers.get('x-admin-key') || request.nextUrl.searchParams.get('key');
|
||||
if (provided !== adminKey) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const tokens = await getValidTiktokTokens();
|
||||
if (!tokens) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No TikTok account connected. Visit /api/tiktok/connect first.' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
access_token: tokens.accessToken,
|
||||
open_id: tokens.openId,
|
||||
scope: tokens.scope,
|
||||
expires_at: tokens.accessTokenExpiresAt.toISOString(),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('TikTok token endpoint error:', err);
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 502 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user