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
|
size_bytes: int
|
||||||
sort_order: int
|
sort_order: int
|
||||||
uploaded_at: datetime
|
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 logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -15,9 +16,11 @@ from pathlib import Path
|
|||||||
from fastapi import APIRouter, File, Header, HTTPException, Request, UploadFile
|
from fastapi import APIRouter, File, Header, HTTPException, Request, UploadFile
|
||||||
from supabase import Client, create_client
|
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 .dsm_client import DSMError, create_share_link
|
||||||
from .models import (
|
from .models import (
|
||||||
|
MintTokenRequest,
|
||||||
|
MintTokenResponse,
|
||||||
PackFileItem,
|
PackFileItem,
|
||||||
SignLinkRequest,
|
SignLinkRequest,
|
||||||
SignLinkResponse,
|
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"}
|
ALLOWED_EXT = {"pdf", "zip", "mp4", "mov", "mkv", "wav", "m4a", "mp3", "png", "jpg", "jpeg", "webp", "prj"}
|
||||||
MAX_BYTES = 5 * 1024 * 1024 * 1024 # 5GB
|
MAX_BYTES = 5 * 1024 * 1024 * 1024 # 5GB
|
||||||
SAFE_FILENAME = re.compile(r"^[\w가-힣\-\.\(\)\s]+$")
|
SAFE_FILENAME = re.compile(r"^[\w가-힣\-\.\(\)\s]+$")
|
||||||
|
UPLOAD_TOKEN_TTL_SEC = int(os.getenv("UPLOAD_TOKEN_TTL_SEC", "1800")) # 30분 default
|
||||||
|
|
||||||
|
|
||||||
def _supabase() -> Client:
|
def _supabase() -> Client:
|
||||||
@@ -76,6 +80,34 @@ async def sign_link(
|
|||||||
return SignLinkResponse(url=url, expires_at=expires_at)
|
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)
|
@router.post("/upload", response_model=UploadResponse)
|
||||||
async def upload(
|
async def upload(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
|
|||||||
@@ -100,3 +100,39 @@ def test_list_success(mock_sb):
|
|||||||
r = client.get("/api/packs/list", headers=_signed(body))
|
r = client.get("/api/packs/list", headers=_signed(body))
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert len(r.json()) == 1
|
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