feat(packs-lab): POST /api/packs/admin/mint-token 라우트 + 통합 테스트

MintTokenRequest/Response 스키마 추가, mint_token 라우트 구현 (HMAC 인증 + 확장자 검증 + JTI 발급), 테스트 3건 추가.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 01:27:43 +09:00
parent ef026e7ac6
commit dc482b32e4
3 changed files with 83 additions and 1 deletions

View File

@@ -37,3 +37,17 @@ class PackFileItem(BaseModel):
size_bytes: int
sort_order: int
uploaded_at: datetime
class MintTokenRequest(BaseModel):
"""Vercel → backend: admin upload 토큰 발급 요청."""
tier: PackTier
label: str = Field(..., max_length=200)
filename: str = Field(..., max_length=255)
size_bytes: int = Field(..., gt=0, le=5 * 1024 * 1024 * 1024)
class MintTokenResponse(BaseModel):
token: str
expires_at: datetime
jti: str

View File

@@ -8,6 +8,7 @@
import logging
import os
import re
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
@@ -15,9 +16,11 @@ from pathlib import Path
from fastapi import APIRouter, File, Header, HTTPException, Request, UploadFile
from supabase import Client, create_client
from .auth import verify_request_hmac, verify_upload_token
from .auth import mint_upload_token, verify_request_hmac, verify_upload_token
from .dsm_client import DSMError, create_share_link
from .models import (
MintTokenRequest,
MintTokenResponse,
PackFileItem,
SignLinkRequest,
SignLinkResponse,
@@ -31,6 +34,7 @@ PACK_BASE_DIR = Path("/volume1/docker/webpage/media/packs")
ALLOWED_EXT = {"pdf", "zip", "mp4", "mov", "mkv", "wav", "m4a", "mp3", "png", "jpg", "jpeg", "webp", "prj"}
MAX_BYTES = 5 * 1024 * 1024 * 1024 # 5GB
SAFE_FILENAME = re.compile(r"^[\w가-힣\-\.\(\)\s]+$")
UPLOAD_TOKEN_TTL_SEC = int(os.getenv("UPLOAD_TOKEN_TTL_SEC", "1800")) # 30분 default
def _supabase() -> Client:
@@ -76,6 +80,34 @@ async def sign_link(
return SignLinkResponse(url=url, expires_at=expires_at)
@router.post("/admin/mint-token", response_model=MintTokenResponse)
async def mint_token(
request: Request,
x_timestamp: str = Header(""),
x_signature: str = Header(""),
):
body = await request.body()
verify_request_hmac(body, x_timestamp, x_signature)
payload = MintTokenRequest.model_validate_json(body)
_check_filename(payload.filename)
jti = str(uuid.uuid4())
expires_ts = int(time.time()) + UPLOAD_TOKEN_TTL_SEC
token = mint_upload_token({
"tier": payload.tier,
"label": payload.label,
"filename": payload.filename,
"size_bytes": payload.size_bytes,
"jti": jti,
"expires_at": expires_ts,
})
return MintTokenResponse(
token=token,
expires_at=datetime.fromtimestamp(expires_ts, tz=timezone.utc),
jti=jti,
)
@router.post("/upload", response_model=UploadResponse)
async def upload(
file: UploadFile = File(...),