From 83192eb66c1171d31705a1e2ac1ff882eb44f335 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 5 May 2026 18:51:24 +0900 Subject: [PATCH] =?UTF-8?q?docs(spec):=20packs-lab=20=EC=9D=B8=ED=94=84?= =?UTF-8?q?=EB=9D=BC=20=ED=86=B5=ED=95=A9=20+=20admin=20mint-token=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /api/packs/admin/mint-token (Vercel HMAC → 일회성 upload 토큰) - Supabase pack_files DDL + 활성/삭제 인덱스 - docker-compose 18950 + nginx 5GB streaming + .env.example 6+1 환경변수 - tests: routes 통합 + DSM client mock + autouse HMAC fixture - CLAUDE.md: web-backend 5곳 + workspace 1곳 갱신 - DELETE 라우트 docstring 정리(자동 만료) Co-Authored-By: Claude Opus 4.7 (1M context) --- ...5-05-packs-lab-infra-integration-design.md | 446 ++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-05-packs-lab-infra-integration-design.md diff --git a/docs/superpowers/specs/2026-05-05-packs-lab-infra-integration-design.md b/docs/superpowers/specs/2026-05-05-packs-lab-infra-integration-design.md new file mode 100644 index 0000000..27ca5ed --- /dev/null +++ b/docs/superpowers/specs/2026-05-05-packs-lab-infra-integration-design.md @@ -0,0 +1,446 @@ +# packs-lab 인프라 통합 + admin mint-token 설계 + +> 대상: `web-backend/packs-lab/` +> 외부 의존: Supabase(`pack_files` 테이블) + Vercel SaaS(HMAC 호출자) +> 후속 별도 스펙: Vercel-side admin UI / 사용자 다운로드 / cleanup cron / multi-admin + +--- + +## 1. 목표 + +`packs-lab`은 NAS 자료 다운로드 자동화 백엔드. Synology DSM 공유 링크 발급 + 5GB 멀티파트 업로드 수신을 담당하고, Vercel SaaS와 HMAC으로 통신한다. 사용자 인증은 Vercel이 Supabase로 처리하고 본 서비스는 외부 인증을 다루지 않는다. + +이미 코드(HMAC 미들웨어 / DSM client / 4 라우트)는 작성되어 있으나 인프라 통합 + Supabase 스키마 + admin upload 토큰 발급 흐름이 빠져 있어 운영 가능 상태가 아니다. 본 스펙은 그 갭을 메운다. + +### 핵심 변경 + +- **신규 라우트**: `POST /api/packs/admin/mint-token` (Vercel HMAC → 일회성 업로드 토큰) +- **Supabase DDL**: `pack_files` 테이블 + 활성·삭제 인덱스 +- **인프라**: docker-compose `packs-lab` 서비스 등록(18950) + nginx `/api/packs/` 5GB 통과 + `.env.example` 6+1 환경변수 +- **테스트**: routes 통합 + DSM client mock +- **문서**: web-backend / workspace CLAUDE.md 5곳 갱신 +- **DELETE 라우트 docstring**: "DSM 공유 정리" 표현을 "DSM 공유 자동 만료"로 수정 (실제 동작과 일치) + +### 변경하지 않는 것 + +- 기존 `auth.py` (`mint_upload_token` 그대로 활용) +- 기존 `dsm_client.py` +- 기존 `routes.py`의 sign-link / upload / list / delete 본문 +- DSM 공유 추적 테이블 — 4시간 자동 만료로 충분(브레인스토밍 결정) + +--- + +## 2. 컴포넌트 + 통신 흐름 + +### 2.1 변경 받는 파일 + +| 영역 | 파일 | 변경 | +|------|------|------| +| 백엔드 | `packs-lab/app/routes.py` | DELETE docstring 수정 + admin mint-token 라우트 추가 | +| 백엔드 | `packs-lab/app/models.py` | `MintTokenRequest`, `MintTokenResponse` 스키마 추가 | +| 백엔드 | `packs-lab/app/auth.py` | 변경 없음 (기존 `mint_upload_token` 활용) | +| 테스트 | `packs-lab/tests/conftest.py` (신규) | autouse `BACKEND_HMAC_SECRET` 셋팅 | +| 테스트 | `packs-lab/tests/test_routes.py` (신규) | 5 라우트 통합 테스트 | +| 테스트 | `packs-lab/tests/test_dsm_client.py` (신규) | DSM 7.x API mock 테스트 | +| DB | `packs-lab/supabase/pack_files.sql` (신규) | DDL + 인덱스 | +| 인프라 | `docker-compose.yml` | `packs-lab` 서비스 추가 | +| 인프라 | `nginx/default.conf` | `/api/packs/` 라우팅 (`client_max_body_size 5G` + streaming) | +| 인프라 | `.env.example` | 6+1 신규 환경변수 | +| 문서 | `web-backend/CLAUDE.md` | 1·4·5·8·9 섹션 갱신 | +| 문서 | `workspace/CLAUDE.md` | 컨테이너 표 한 줄 추가 | + +### 2.2 통신 흐름 + +**ADMIN 업로드** + +``` +Vercel admin UI ─────→ Vercel API (HMAC 헤더 추가) + │ + ▼ + POST /api/packs/admin/mint-token + │ + backend: verify_request_hmac + │ + mint_upload_token({tier, label, filename, size_bytes, jti, expires_at}) + │ +Vercel ←─────────────── token ──────┘ + │ + ▼ +admin browser → POST /api/packs/upload + Authorization: Bearer + multipart body (≤5GB) + │ + backend: verify_upload_token + JTI mark + │ + 파일 저장 (PACK_BASE_DIR/{tier}/{filename}) + │ + Supabase INSERT pack_files +``` + +**사용자 다운로드** + +``` +사용자 → Vercel SaaS (Supabase auth + tier·결제 검증) + │ + ▼ + POST /api/packs/sign-link (HMAC + file_path) + │ + backend: verify_request_hmac + │ + DSM Sharing.create (4시간 만료) + │ +사용자 ← Vercel ← 다운로드 URL (4시간 유효) +``` + +### 2.3 기각된 대안 + +| 대안 | 기각 사유 | +|------|-----------| +| Vercel-side 토큰 발급 | 토큰 포맷 양쪽 분산, 변경 시 동기화 부담 | +| admin browser → backend 직접 HMAC | admin browser에 secret 노출, 보안 약화 | +| DSM 공유 추적 테이블 | 4시간 자동 만료로 충분, YAGNI | +| Resumable multipart upload | 5GB는 단일 stream으로 충분, 복잡도 증가 | +| `pack_files.min_tier`를 PostgreSQL ENUM | tier 추가 시 ALTER TYPE 번거로움. text+CHECK 채택 | + +--- + +## 3. `POST /api/packs/admin/mint-token` + +### 3.1 Pydantic 스키마 (`models.py` 추가) + +```python +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 +``` + +### 3.2 라우트 본문 (`routes.py` 추가) + +```python +import time, uuid +from datetime import datetime, timezone + +from .auth import mint_upload_token, verify_request_hmac +from .models import MintTokenRequest, MintTokenResponse + +UPLOAD_TOKEN_TTL_SEC = int(os.getenv("UPLOAD_TOKEN_TTL_SEC", "1800")) # 30분 default + +@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) # upload 라우트와 동일 검증 + + 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, + ) +``` + +### 3.3 결정 근거 + +| 항목 | 값 | 근거 | +|------|-----|------| +| TTL default | 1800s (30분) | 5GB 업로드 시작 + 진행 시간 여유. 1Gbps에서 약 40s, 50Mbps에서 약 14분 | +| TTL env override | `UPLOAD_TOKEN_TTL_SEC` | 운영 중 조정 가능 | +| filename 검증 | upload와 동일 (`_check_filename`) | 토큰 발급 시점에 미리 거부 → admin UI 즉시 피드백 | +| jti 응답 포함 | yes | admin이 업로드 결과 추적용 | +| Vercel ↔ backend | HMAC (`X-Timestamp` + `X-Signature`) | 다른 admin 라우트와 동일 패턴 | +| admin browser ↔ backend | Bearer token (단발성 jti) | 기존 upload 라우트 그대로 | + +### 3.4 DELETE 라우트 docstring 수정 + +`routes.py` 모듈 docstring에서: + +```diff +- DELETE /api/packs/{file_id} — Vercel HMAC 인증 → soft delete + DSM 공유 정리 ++ DELETE /api/packs/{file_id} — Vercel HMAC 인증 → soft delete (DSM 공유는 자동 만료) +``` + +`delete_file` 함수에는 변경 없음. + +--- + +## 4. Supabase `pack_files` DDL + +**파일**: `packs-lab/supabase/pack_files.sql` (신규, 운영 배포 시 Supabase SQL editor에서 실행) + +```sql +-- pack_files: NAS에 저장된 다운로드 가능한 패키지 파일 메타 +create table if not exists public.pack_files ( + id uuid primary key default gen_random_uuid(), + min_tier text not null check (min_tier in ('starter','pro','master')), + label text not null, + file_path text not null unique, -- NAS 절대경로, 동일 경로 중복 방지 + filename text not null, + size_bytes bigint not null check (size_bytes > 0), + sort_order integer not null default 0, + uploaded_at timestamptz not null default now(), + deleted_at timestamptz +); + +-- list 라우트의 hot path: deleted_at IS NULL + tier/order 정렬 +create index if not exists pack_files_active_idx + on public.pack_files (min_tier, sort_order) + where deleted_at is null; + +-- soft-deleted 통계 / cleanup 잡 대비 +create index if not exists pack_files_deleted_at_idx + on public.pack_files (deleted_at) + where deleted_at is not null; +``` + +### 4.1 필드 결정 근거 + +| 필드 | 타입 / 제약 | 근거 | +|------|------------|------| +| `id` | uuid PK + `gen_random_uuid()` default | routes.py가 client-side `uuid.uuid4()` 생성하지만 default도 둬 fallback | +| `min_tier` | text + CHECK | enum 대신 text+CHECK가 PostgreSQL에서 더 유연 | +| `file_path` | text NOT NULL UNIQUE | 같은 tier/filename 충돌은 파일시스템에서 잡지만 DB 레벨도 보강 | +| `size_bytes` | bigint + CHECK > 0 | 5GB는 int 범위 안이지만 미래 대비 bigint | +| `sort_order` | int NOT NULL default 0 | routes INSERT가 sort_order 미지정 → 0 기본 | +| `uploaded_at` | timestamptz default now() | routes 코드가 `res.data[0]["uploaded_at"]` 그대로 응답에 사용 — DB가 채워줌 | +| `deleted_at` | nullable | soft delete | + +### 4.2 RLS + +비활성. backend가 `service_role` key 사용하므로 RLS 우회. Vercel/사용자 직접 접근 없음 → unsafe 아님. + +--- + +## 5. 인프라 통합 + +### 5.1 `docker-compose.yml` — `packs-lab` 서비스 + +```yaml + packs-lab: + build: + context: ./packs-lab + dockerfile: Dockerfile + container_name: packs-lab + restart: unless-stopped + ports: + - "18950:8000" + environment: + TZ: Asia/Seoul + DSM_HOST: ${DSM_HOST} + DSM_USER: ${DSM_USER} + DSM_PASS: ${DSM_PASS} + BACKEND_HMAC_SECRET: ${BACKEND_HMAC_SECRET} + SUPABASE_URL: ${SUPABASE_URL} + SUPABASE_SERVICE_KEY: ${SUPABASE_SERVICE_KEY} + UPLOAD_TOKEN_TTL_SEC: ${UPLOAD_TOKEN_TTL_SEC:-1800} + volumes: + - ${PACK_DATA_PATH:-./data/packs}:/volume1/docker/webpage/media/packs +``` + +| 결정 | 값 | 근거 | +|------|-----|------| +| 포트 | 18950 | 18800(realestate) → 18900(agent-office) → 18950(packs) 순차 | +| `PACK_BASE_DIR` 마운트 | 컨테이너 경로 `/volume1/docker/webpage/media/packs` 고정 | routes.py 하드코딩 경로 | +| `PACK_DATA_PATH` env | default `./data/packs` (로컬), NAS `.env`에 `/volume1/docker/webpage/media/packs` 명시 | 운영/로컬 분리 | + +### 5.2 `nginx/default.conf` — `/api/packs/` 라우팅 + +```nginx +location /api/packs/ { + proxy_pass http://packs-lab:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 5GB 멀티파트 업로드 대응 + client_max_body_size 5G; + proxy_request_buffering off; # 스트리밍 통과 (메모리/디스크 buffer 회피) + proxy_read_timeout 1800s; + proxy_send_timeout 1800s; +} +``` + +| 결정 | 근거 | +|------|------| +| `client_max_body_size 5G` | 라우트 단위 — 다른 location은 default 유지 | +| `proxy_request_buffering off` | 5GB 파일을 nginx가 모두 받고 backend에 forward하면 ~5GB 디스크 buffer 발생 | +| `proxy_read/send_timeout 1800s` | 30분 — 업로드 토큰 TTL과 일치, 느린 업링크에서 5GB 전송 여유 | + +### 5.3 `.env.example` — 6+1 신규 환경변수 + +```bash +# ─── packs-lab — NAS 자료 다운로드 자동화 ──────────────────────────── +# Synology DSM 7.x 인증 (공유 링크 발급용) +DSM_HOST=https://gahusb.synology.me:5001 +DSM_USER= +DSM_PASS= + +# Vercel SaaS ↔ backend HMAC 시크릿 (양쪽 동일 값) +BACKEND_HMAC_SECRET= + +# Supabase pack_files 테이블 접근 (service_role 키, RLS 우회) +SUPABASE_URL=https://.supabase.co +SUPABASE_SERVICE_KEY= + +# admin upload 토큰 TTL (초). default 1800 = 30분 +UPLOAD_TOKEN_TTL_SEC=1800 + +# 로컬 개발: ./data/packs / NAS 운영: /volume1/docker/webpage/media/packs +PACK_DATA_PATH=./data/packs +``` + +### 5.4 NAS 디렉토리 준비 + +운영 첫 배포 시 SSH로 1회: + +```bash +mkdir -p /volume1/docker/webpage/media/packs/{starter,pro,master} +chown -R PUID:PGID /volume1/docker/webpage/media/packs +``` + +PUID/PGID는 `.env`의 기존 값 사용. + +--- + +## 6. 테스트 전략 + +기존 `tests/test_auth.py` 유지. 신규 3 파일. + +### 6.1 `tests/conftest.py` (신규) + +```python +import pytest + +@pytest.fixture(autouse=True) +def _hmac_secret(monkeypatch): + """모든 테스트에서 동일한 HMAC secret 사용.""" + monkeypatch.setenv("BACKEND_HMAC_SECRET", "test-secret-do-not-use-in-prod") +``` + +### 6.2 `tests/test_routes.py` (신규) — 통합 테스트 + +DSM·Supabase 모두 mock. `pytest`, `monkeypatch`, `unittest.mock`, `fastapi.testclient.TestClient` 사용. + +| 테스트 | 검증 | +|--------|------| +| `test_sign_link_hmac_required` | timestamp/signature 헤더 누락 → 401 | +| `test_sign_link_outside_base_dir` | file_path가 `PACK_BASE_DIR` 외부 → 400 | +| `test_sign_link_calls_dsm` | mock된 `create_share_link` 호출 검증, URL 응답 | +| `test_mint_token_hmac_required` | HMAC 누락 → 401 | +| `test_mint_token_returns_valid_token` | 발급된 token이 `verify_upload_token`으로 통과 | +| `test_mint_token_invalid_filename` | 확장자 미허용 → 400 | +| `test_upload_token_required` | Authorization Bearer 누락 → 401 | +| `test_upload_size_mismatch` | 토큰 size_bytes ≠ 실제 → 400 | +| `test_upload_jti_replay` | 같은 토큰 두 번 → 두 번째 409 | +| `test_list_returns_active_only` | mock supabase 응답에서 deleted_at NULL만 반환 | +| `test_delete_soft_deletes` | mock supabase update에 deleted_at ISO timestamp 들어감 | + +### 6.3 `tests/test_dsm_client.py` (신규) + +httpx mock(`respx` 또는 `MockTransport`) 또는 `monkeypatch.setattr` 패치. + +| 테스트 | 검증 | +|--------|------| +| `test_create_share_link_login_logout` | login → Sharing.create → logout 순서 | +| `test_create_share_link_returns_url_and_expiry` | 응답 파싱 | +| `test_dsm_login_failure_raises` | login API success=false → DSMError | +| `test_dsm_share_failure_logs_out` | Sharing.create 실패해도 logout 호출 (try/finally) | + +--- + +## 7. 문서 갱신 + +### 7.1 `web-backend/CLAUDE.md` — 5곳 + +**1. 1.프로젝트 개요** + +```diff +- 서비스: lotto-lab, stock-lab, travel-proxy, music-lab, blog-lab, realestate-lab, agent-office, personal, deployer (9개) ++ 서비스: lotto-lab, stock-lab, travel-proxy, music-lab, blog-lab, realestate-lab, agent-office, personal, packs-lab, deployer (10개) +``` + +**2. 4.Docker 서비스 표** — 신규 행 + +``` +| `packs-lab` | 18950 | NAS 자료 다운로드 자동화 (DSM 공유 링크 + 5GB 업로드, Vercel SaaS와 HMAC 통신) | +``` + +**3. 5.Nginx 라우팅 표** — 신규 행 + +``` +| `/api/packs/` | `packs-lab:8000` | 5GB 업로드 (`client_max_body_size 5G` + `proxy_request_buffering off`) | +``` + +**4. 8.로컬 개발 표** — 신규 행 + +``` +| Packs Lab | http://localhost:18950 | +``` + +**5. 9.서비스별** — `### packs-lab (packs-lab/)` 신규 섹션 + +내용: +- 용도 (NAS DSM 공유링크 + 5GB 업로드 + Vercel HMAC, 사용자 인증은 Vercel이 Supabase로 처리) +- 환경변수 6+1개 +- DB는 외부 Supabase `pack_files` (DDL은 `packs-lab/supabase/pack_files.sql`) +- 파일 구조: `main.py`, `auth.py`, `dsm_client.py`, `routes.py`, `models.py` +- API 표 5개: + - `POST /api/packs/sign-link` (Vercel HMAC → DSM Sharing.create) + - `POST /api/packs/admin/mint-token` (Vercel HMAC → upload 토큰) + - `POST /api/packs/upload` (Bearer token → multipart 5GB) + - `GET /api/packs/list` (Vercel HMAC → 활성 파일 목록) + - `DELETE /api/packs/{file_id}` (Vercel HMAC → soft delete) + +### 7.2 `workspace/CLAUDE.md` + +컨테이너 표에 한 줄 추가: + +``` +| `packs-lab` | 18950 | NAS 자료 다운로드 자동화 (Vercel SaaS와 HMAC 통신) | +``` + +--- + +## 8. 스코프 + +### 본 spec 범위 + +- ✅ admin mint-token 라우트 신설 +- ✅ Supabase `pack_files` DDL +- ✅ docker-compose / nginx / .env.example / NAS 디렉토리 마운트 +- ✅ tests (auth 유지 + routes 통합 + dsm_client mock) +- ✅ CLAUDE.md 2곳 갱신 +- ✅ DELETE 라우트 docstring 수정 + +### 후속 별도 spec + +- ❌ Vercel SaaS-side admin UI / 사용자 다운로드 UI / Supabase pricing & user 테이블 +- ❌ DSM 공유 추적 (즉시 차단 필요시) +- ❌ deleted_at + N일 후 실제 파일 삭제 cron +- ❌ multi-admin 토큰 발급 권한 분리 +- ❌ resumable multipart 업로드 (5GB tus 등) +- ❌ pack_files sort_order 편집 endpoint (admin UI 단계) +- ❌ monitoring (업로드 실패율, DSM API latency)