import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; // Social asset hosting for TikTok PULL_FROM_URL (and other social APIs that // only accept public URLs on a verified domain). Assets are stored in // PostgreSQL so they survive container redeploys without extra volumes and // are served from qrmaster.net via GET /api/social-assets/[id]. const isAdminRequest = (request: NextRequest) => { const adminKey = process.env.TIKTOK_ADMIN_KEY; if (!adminKey) return false; const provided = request.headers.get('x-admin-key') || request.nextUrl.searchParams.get('key'); return provided === adminKey; }; const MAX_FILE_BYTES = 10 * 1024 * 1024; const ALLOWED_MIME_TYPES = new Set([ 'image/jpeg', 'image/png', 'image/webp', 'video/mp4', ]); // The "SocialAsset" table is NOT created by this route. Apply the schema // manually on the server (npm run docker:db), see // docs/automations/social-accounts-and-jobs.md. export async function POST(request: NextRequest) { if (!isAdminRequest(request)) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const json = await request.json().catch(() => null); const rawFiles = Array.isArray(json?.files) ? json.files : json ? [json] : []; if (!rawFiles.length) { return NextResponse.json( { error: 'Provide { files: [{ filename, mimeType, dataBase64 }] }.' }, { status: 400 } ); } const files: Array<{ filename: string; mimeType: string; data: Buffer }> = []; for (const file of rawFiles) { const filename = String(file?.filename || '').trim(); const mimeType = String(file?.mimeType || '').trim().toLowerCase(); const dataBase64 = String(file?.dataBase64 || ''); if (!filename || !mimeType || !dataBase64) { return NextResponse.json( { error: 'Each file needs filename, mimeType and dataBase64.' }, { status: 400 } ); } if (!ALLOWED_MIME_TYPES.has(mimeType)) { return NextResponse.json( { error: `Unsupported mimeType: ${mimeType}` }, { status: 400 } ); } const data = Buffer.from(dataBase64, 'base64'); if (!data.length || data.length > MAX_FILE_BYTES) { return NextResponse.json( { error: `File ${filename} is empty or exceeds ${MAX_FILE_BYTES} bytes.` }, { status: 400 } ); } files.push({ filename, mimeType, data }); } try { const baseUrl = process.env.NEXTAUTH_URL || 'https://qrmaster.net'; const assets = []; for (const file of files) { const asset = await db.socialAsset.create({ data: { filename: file.filename, mimeType: file.mimeType, data: file.data, }, select: { id: true, filename: true, mimeType: true, createdAt: true }, }); assets.push({ ...asset, url: `${baseUrl.replace(/\/$/, '')}/api/social-assets/${asset.id}`, }); } return NextResponse.json({ assets }); } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; return NextResponse.json({ error: message }, { status: 500 }); } } export async function GET(request: NextRequest) { if (!isAdminRequest(request)) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } try { const assets = await db.socialAsset.findMany({ select: { id: true, filename: true, mimeType: true, createdAt: true }, orderBy: { createdAt: 'desc' }, take: 100, }); return NextResponse.json({ assets }); } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; return NextResponse.json({ error: message }, { status: 500 }); } }