refactor: update Docker base images, enhance image upload security, and implement shared form client logic and validation
This commit is contained in:
@@ -1,4 +1,17 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
import { requireAdmin } from "@/lib/auth";
|
||||
|
||||
const TIKTOK_OAUTH_STATE_COOKIE = "tiktok-oauth-state";
|
||||
|
||||
function escapeHtml(value: unknown) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/auth/tiktok/callback
|
||||
@@ -7,10 +20,20 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
* Displays the tokens so you can copy them to your .env file.
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
|
||||
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") },
|
||||
@@ -52,11 +75,11 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
);
|
||||
|
||||
const tokenData = await tokenRes.json();
|
||||
const tokenData = (await tokenRes.json()) as Record<string, unknown>;
|
||||
|
||||
if (tokenData.error) {
|
||||
return NextResponse.json(
|
||||
{ error: tokenData.error, description: tokenData.error_description },
|
||||
{ error: tokenData.error, description: tokenData.error_description },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -71,20 +94,20 @@ export async function GET(request: NextRequest) {
|
||||
<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>${tokenData.access_token}</code></p>
|
||||
<p><strong>TIKTOK_REFRESH_TOKEN=</strong><br/><code>${tokenData.refresh_token}</code></p>
|
||||
<p><strong>TIKTOK_OPEN_ID=</strong><br/><code>${tokenData.open_id}</code></p>
|
||||
<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(tokenData.expires_in / 3600)} hours.<br/>
|
||||
Refresh token expires in ${Math.floor(tokenData.refresh_expires_in / 86400)} days.<br/>
|
||||
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;">${JSON.stringify(tokenData, null, 2)}</pre>
|
||||
<pre style="background: #1a1a1a; padding: 12px; border-radius: 6px; overflow-x: auto;">${escapeHtml(JSON.stringify(tokenData, null, 2))}</pre>
|
||||
</details>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { cookies } from "next/headers";
|
||||
import { requireAdmin } from "@/lib/auth";
|
||||
|
||||
const TIKTOK_OAUTH_STATE_COOKIE = "tiktok-oauth-state";
|
||||
|
||||
/**
|
||||
* GET /api/auth/tiktok
|
||||
@@ -6,6 +11,8 @@ import { NextResponse } from "next/server";
|
||||
* Visit this URL once to start the OAuth flow and get your refresh token.
|
||||
*/
|
||||
export async function GET() {
|
||||
|
||||
|
||||
const clientKey = process.env.TIKTOK_CLIENT_KEY;
|
||||
const redirectUri = `${process.env.BASE_URL}/api/auth/tiktok/callback`;
|
||||
|
||||
@@ -16,7 +23,7 @@ export async function GET() {
|
||||
);
|
||||
}
|
||||
|
||||
const csrfState = Math.random().toString(36).substring(2);
|
||||
const csrfState = randomBytes(32).toString("hex");
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_key: clientKey,
|
||||
@@ -28,5 +35,14 @@ export async function GET() {
|
||||
|
||||
const authUrl = `https://www.tiktok.com/v2/auth/authorize/?${params.toString()}`;
|
||||
|
||||
return NextResponse.redirect(authUrl);
|
||||
const response = NextResponse.redirect(authUrl);
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(TIKTOK_OAUTH_STATE_COOKIE, csrfState, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: 600,
|
||||
path: "/api/auth/tiktok",
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user