From 3f0c5e7f1c0d0669e25c9d3331100c5055db20c8 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 2 May 2026 09:04:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(api):=20/api/admin/packs/upload-url=20?= =?UTF-8?q?=E2=80=94=20admin=20=EC=9D=BC=ED=9A=8C=EC=84=B1=20HMAC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=86=A0=ED=81=B0=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 15분 만료 + jti 단발성. 브라우저는 이 토큰을 web-backend /api/packs/upload에 직접 multipart POST 시 Authorization Bearer 헤더로 전달 → Vercel function body limit 우회 (5GB 업로드). Co-Authored-By: Claude Opus 4.7 (1M context) --- app/api/admin/packs/upload-url/route.ts | 51 +++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 app/api/admin/packs/upload-url/route.ts diff --git a/app/api/admin/packs/upload-url/route.ts b/app/api/admin/packs/upload-url/route.ts new file mode 100644 index 0000000..3522279 --- /dev/null +++ b/app/api/admin/packs/upload-url/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { verifyAdminTokenNode } from '@/lib/admin-auth'; +import { mintUploadToken } from '@/lib/web-backend'; +import type { PackTier } from '@/lib/pack-assets'; + +export const runtime = 'nodejs'; + +async function checkAuth() { + const cookieStore = await cookies(); + const token = cookieStore.get('admin_token')?.value; + return token && verifyAdminTokenNode(token); +} + +const VALID_TIERS = new Set(['starter', 'pro', 'master']); +const MAX_BYTES = 5 * 1024 * 1024 * 1024; +const ALLOWED_EXT = new Set(['pdf', 'zip', 'mp4', 'mov', 'mkv', 'wav', 'm4a', 'mp3', 'png', 'jpg', 'jpeg', 'webp', 'prj']); + +export async function POST(request: Request) { + if (!(await checkAuth())) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { tier, label, filename, sizeBytes } = await request.json(); + + if (!VALID_TIERS.has(tier)) { + return NextResponse.json({ error: 'tier 유효하지 않음' }, { status: 400 }); + } + if (!label || typeof label !== 'string' || label.length > 200) { + return NextResponse.json({ error: 'label 필요 (1-200자)' }, { status: 400 }); + } + if (!filename || typeof filename !== 'string') { + return NextResponse.json({ error: 'filename 필요' }, { status: 400 }); + } + const ext = filename.includes('.') ? filename.split('.').pop()!.toLowerCase() : ''; + if (!ALLOWED_EXT.has(ext)) { + return NextResponse.json({ error: `허용되지 않은 확장자: ${ext}` }, { status: 400 }); + } + if (typeof sizeBytes !== 'number' || sizeBytes <= 0 || sizeBytes > MAX_BYTES) { + return NextResponse.json({ error: '파일 크기 0-5GB' }, { status: 400 }); + } + + const { token, uploadUrl, expiresAt } = mintUploadToken({ + tier, + label, + filename, + size_bytes: sizeBytes, + }); + + return NextResponse.json({ token, uploadUrl, expiresAt }); +}