TikTok V5 + Security
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { TIKTOK_ACCOUNT_KEY, TIKTOK_OAUTH_STATE_COOKIE_NAME } from '@/lib/tiktok';
|
||||
import {
|
||||
assertExpectedTiktokAccount,
|
||||
TIKTOK_ACCOUNT_KEY,
|
||||
TIKTOK_OAUTH_STATE_COOKIE_NAME,
|
||||
} from '@/lib/tiktok';
|
||||
|
||||
const textResponse = (body: string, status: number) => {
|
||||
const response = new NextResponse(body, {
|
||||
@@ -56,6 +60,9 @@ export async function GET(request: NextRequest) {
|
||||
throw new Error(tokens.error_description || tokens.error || 'TikTok token exchange failed');
|
||||
}
|
||||
|
||||
// Reject the wrong browser account before it can replace the valid QRMaster connection.
|
||||
assertExpectedTiktokAccount(tokens.open_id);
|
||||
|
||||
const now = Date.now();
|
||||
const accessTokenExpiresAt = new Date(now + Number(tokens.expires_in || 0) * 1000);
|
||||
const refreshTokenExpiresAt = tokens.refresh_expires_in
|
||||
@@ -87,6 +94,8 @@ export async function GET(request: NextRequest) {
|
||||
} 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);
|
||||
const status =
|
||||
err instanceof Error && 'status' in err ? (err as { status?: number }).status : 502;
|
||||
return textResponse(`Failed to connect TikTok account: ${message}`, status || 502);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getValidTiktokTokens } from '@/lib/tiktok';
|
||||
import { createHash } from 'crypto';
|
||||
import { TIKTOK_BRAND, getValidTiktokTokens } from '@/lib/tiktok';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const adminKey = process.env.TIKTOK_ADMIN_KEY;
|
||||
@@ -7,7 +8,7 @@ export async function GET(request: NextRequest) {
|
||||
// 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' },
|
||||
{ error: 'TIKTOK_ADMIN_KEY must be configured to expose TikTok status' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
@@ -27,15 +28,28 @@ export async function GET(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const refreshExpired = Boolean(
|
||||
tokens.refreshTokenExpiresAt && tokens.refreshTokenExpiresAt.getTime() <= now
|
||||
);
|
||||
|
||||
// This endpoint is status-only. Access and refresh tokens remain server-side.
|
||||
return NextResponse.json({
|
||||
access_token: tokens.accessToken,
|
||||
open_id: tokens.openId,
|
||||
connected: true,
|
||||
accountMatch: true,
|
||||
brand: TIKTOK_BRAND,
|
||||
openIdFingerprint: createHash('sha256').update(tokens.openId).digest('hex').slice(0, 12),
|
||||
scope: tokens.scope,
|
||||
expires_at: tokens.accessTokenExpiresAt.toISOString(),
|
||||
accessTokenExpiresAt: tokens.accessTokenExpiresAt.toISOString(),
|
||||
refreshTokenExpiresAt: tokens.refreshTokenExpiresAt?.toISOString() || null,
|
||||
requiresReconnect: refreshExpired,
|
||||
uploadMode: 'draft',
|
||||
});
|
||||
} 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 });
|
||||
const status =
|
||||
err instanceof Error && 'status' in err ? (err as { status?: number }).status : 502;
|
||||
return NextResponse.json({ error: message }, { status: status || 502 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user