TikTok V5 + Security

This commit is contained in:
2026-07-11 22:09:10 +02:00
parent 671c1a1559
commit d542f849aa
36 changed files with 740 additions and 463 deletions

View File

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

View File

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