Files
erika/app/api/auth/tiktok/callback/route.ts
T

120 lines
3.9 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { requireAdmin } from "@/lib/auth/auth";
const TIKTOK_OAUTH_STATE_COOKIE = "tiktok-oauth-state";
function escapeHtml(value: unknown) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
/**
* GET /api/auth/tiktok/callback
* Handles the OAuth callback from TikTok.
* Exchanges the authorization code for access + refresh tokens.
* Displays the tokens so you can copy them to your .env file.
*/
export async function GET(request: NextRequest) {
await requireAdmin();
const { searchParams } = new URL(request.url);
const code = searchParams.get("code");
const error = searchParams.get("error");
const cookieStore = await cookies();
const expectedState = cookieStore.get(TIKTOK_OAUTH_STATE_COOKIE)?.value;
const state = searchParams.get("state");
cookieStore.delete(TIKTOK_OAUTH_STATE_COOKIE);
if (!expectedState || !state || state !== expectedState) {
return NextResponse.json({ error: "Invalid OAuth state" }, { status: 400 });
}
if (error) {
return NextResponse.json(
{ error, description: searchParams.get("error_description") },
{ status: 400 }
);
}
if (!code) {
return NextResponse.json(
{ error: "No authorization code received" },
{ status: 400 }
);
}
const clientKey = process.env.TIKTOK_CLIENT_KEY;
const clientSecret = process.env.TIKTOK_CLIENT_SECRET;
const redirectUri = `${process.env.BASE_URL}/api/auth/tiktok/callback`;
if (!clientKey || !clientSecret) {
return NextResponse.json(
{ error: "TIKTOK_CLIENT_KEY or TIKTOK_CLIENT_SECRET not set" },
{ status: 500 }
);
}
// Exchange the code for tokens
const tokenRes = 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 tokenData = (await tokenRes.json()) as Record<string, unknown>;
if (tokenData.error) {
return NextResponse.json(
{ error: tokenData.error, description: tokenData.error_description },
{ status: 400 }
);
}
// Return an HTML page showing the tokens to copy
const html = `
<!DOCTYPE html>
<html>
<head><title>TikTok OAuth Success</title></head>
<body style="font-family: system-ui; max-width: 600px; margin: 40px auto; padding: 20px; background: #0a0a0a; color: #e5e5e5;">
<h1 style="color: #22c55e;">✅ TikTok OAuth Success!</h1>
<p>Copy these values to your <code>.env</code> file:</p>
<div style="background: #1a1a1a; padding: 16px; border-radius: 8px; margin: 16px 0; word-break: break-all;">
<p><strong>TIKTOK_ACCESS_TOKEN=</strong><br/><code>${escapeHtml(tokenData.access_token)}</code></p>
<p><strong>TIKTOK_REFRESH_TOKEN=</strong><br/><code>${escapeHtml(tokenData.refresh_token)}</code></p>
<p><strong>TIKTOK_OPEN_ID=</strong><br/><code>${escapeHtml(tokenData.open_id)}</code></p>
</div>
<p style="color: #a3a3a3; font-size: 14px;">
Access token expires in ${Math.floor(Number(tokenData.expires_in ?? 0) / 3600)} hours.<br/>
Refresh token expires in ${Math.floor(Number(tokenData.refresh_expires_in ?? 0) / 86400)} days.<br/>
The server will auto-refresh the access token using the refresh token.
</p>
<details style="margin-top: 20px;">
<summary style="cursor: pointer;">Full response</summary>
<pre style="background: #1a1a1a; padding: 12px; border-radius: 6px; overflow-x: auto;">${escapeHtml(JSON.stringify(tokenData, null, 2))}</pre>
</details>
</body>
</html>`;
return new NextResponse(html, {
headers: { "Content-Type": "text/html" },
});
}