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:
@@ -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
|
||||
|
||||
@@ -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(...),
|
||||
|
||||
@@ -100,3 +100,39 @@ def test_list_success(mock_sb):
|
||||
r = client.get("/api/packs/list", headers=_signed(body))
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()) == 1
|
||||
|
||||
|
||||
def test_mint_token_hmac_required():
|
||||
"""HMAC 헤더 누락 → 401."""
|
||||
body = {"tier": "pro", "label": "샘플", "filename": "x.zip", "size_bytes": 1024}
|
||||
resp = client.post("/api/packs/admin/mint-token", json=body)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_mint_token_returns_valid_token():
|
||||
"""발급된 token이 verify_upload_token으로 통과해야 한다."""
|
||||
from app.auth import verify_upload_token
|
||||
|
||||
body = {"tier": "pro", "label": "샘플", "filename": "test.zip", "size_bytes": 2048}
|
||||
import json as _json
|
||||
body_bytes = _json.dumps(body).encode()
|
||||
resp = client.post("/api/packs/admin/mint-token", content=body_bytes, headers=_signed(body_bytes))
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "token" in data and "expires_at" in data and "jti" in data
|
||||
|
||||
payload = verify_upload_token(data["token"])
|
||||
assert payload["tier"] == "pro"
|
||||
assert payload["label"] == "샘플"
|
||||
assert payload["filename"] == "test.zip"
|
||||
assert payload["size_bytes"] == 2048
|
||||
assert payload["jti"] == data["jti"]
|
||||
|
||||
|
||||
def test_mint_token_invalid_filename():
|
||||
"""허용 외 확장자 → 400."""
|
||||
body = {"tier": "pro", "label": "샘플", "filename": "x.exe", "size_bytes": 1024}
|
||||
import json as _json
|
||||
body_bytes = _json.dumps(body).encode()
|
||||
resp = client.post("/api/packs/admin/mint-token", content=body_bytes, headers=_signed(body_bytes))
|
||||
assert resp.status_code == 400
|
||||
|
||||
Reference in New Issue
Block a user