From 29da62dc3be27275cc3feb53f8569543d58e5007 Mon Sep 17 00:00:00 2001 From: SlavaKuntsov Date: Sun, 4 Oct 2026 17:52:01 +0300 Subject: [PATCH 1/2] chore(repo): add zcodeignore Ignore rules for ZCode agent workspace context: env files (keep .env.example), python caches, node_modules, build artifacts. --- .zcodeignore | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .zcodeignore diff --git a/.zcodeignore b/.zcodeignore new file mode 100644 index 0000000..9666e13 --- /dev/null +++ b/.zcodeignore @@ -0,0 +1,102 @@ +# Env — keep .env.example +.env +.env.local +.env.*.local +!.env.example + +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +*.pycache +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.hypothesis/ +.coverage +coverage.xml +htmlcov/ +.tox/ +*.cover + +# uv +.uv/ +# keep uv.lock but ignore overrides — intentionally tracked +!.uv.lock +!services/auth/uv.lock +!services/transcoder/uv.lock + +# Node / Frontend +node_modules/ +dist/ +.next/ +out/ +*.tsbuildinfo +.parcel-cache/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnp/ +.pnp.js + +# Go +vendor/ +*.exe +*.out +*.test +bin/ + +# IDE / Editor +.idea/ +.vscode/ +*.swp +*.swo +*~ +.air.toml +.air/ + +# OS +.DS_Store +Thumbs.db + +# Data / tmp — runtime volumes +tmp/ +data/ +pgdata/ +miniodata/ + +# Secrets / local overrides +*.pem +*.key + +# ===== ↑ 以上同步自 .gitignore(「从 .gitignore 同步」只重写以上部分)===== +.git/ +.hg/ +.svn/ +bower_components/ +jspm_packages/ +site-packages/ +coverage/ +lcov-report/ +cmakefiles/ +cmake-build-*/ +bazel-*/ +pods/ +deriveddata/ +storybook-static/ +playwright-report/ +test-results/ +allure-results/ +allure-report/ +cdk.out/ +*.egg-info/ +*.dist-info/ +eggs/ +pip-wheel-metadata/ +wheels/ +# ----- ↑ 以上为 ZCode 默认排除规则(自定义规则请写在本行下方,不会被同步/恢复改动)----- +# 自定义规则写在下方(本行提示可删除) From 8bdbdc0bf151249b068a6e0c8fddbd4d8a1f1e04 Mon Sep 17 00:00:00 2001 From: SlavaKuntsov Date: Sun, 4 Oct 2026 17:52:01 +0300 Subject: [PATCH 2/2] docs: sync spec and pipeline with multipart upload and hls token - upload: s3 multipart is the primary path (issue #56) - 8MB presigned chunks direct to minio, legacy proxy/presign/resumable as fallbacks - gateway: hls-token endpoint (15m signed jwt) and HLSAuth private video protection with metadata lru cache (issue #54) - auth: /me requires type=access (issue #76), refresh rotation with jti revocation in redis - frontend: multipart upload with resume, silent refresh with cross-tab guard, logout kills private player - stack: redis in infra list, nginx-vod internal without published port (issue #43), next.js 14 app router + zustand --- docs/services-pipeline.md | 22 +++++++++++++--------- docs/spec.md | 10 ++++++---- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/services-pipeline.md b/docs/services-pipeline.md index 51607a8..fdbb05f 100644 --- a/docs/services-pipeline.md +++ b/docs/services-pipeline.md @@ -18,16 +18,17 @@ Flowix — MVP видеоплатформы с адаптивным стрими | Сервис | Язык / Фреймворк | Порт | За что отвечает | Хранит / читает | |---|---|---|---|---| -| **gateway** | Go + `chi` | `:8080` | Единая точка входа. Роутит `/api/v1/auth/*` → auth, `/api/v1/videos/*` → metadata/upload, `/hls/*` → nginx-vod. CORS, rate-limit, JWT-проверка, `healthz` | Ничего не хранит, прокси | -| **auth** | Python FastAPI | `:8001` | Регистрация, логин, refresh, `GET /me`. Выпускает JWT (`HS256`, access 15м / refresh 7д, Argon2) | `postgres.users` | +| **gateway** | Go + `chi` | `:8080` | Единая точка входа. Роутит `/api/v1/auth/*` → auth, `/api/v1/videos/*` → metadata/upload, `/hls/*` → nginx-vod. CORS, rate-limit, JWT-проверка, `healthz`; `GET /api/v1/videos/{id}/hls-token` — короткоживущий (15м) подписанный токен для приватных HLS-ссылок | Ничего не хранит, прокси | +| **auth** | Python FastAPI | `:8001` | Регистрация, логин, refresh, `GET /me`. Выпускает JWT (`HS256`, access 15м / refresh 7д, Argon2). Refresh-ротация: активный `jti` хранится в Redis, reuse старого refresh → 401; `/me` принимает только `type=access` (issue #76) | `postgres.users`, Redis (`auth:refresh:`) | | **metadata** | Go + `chi` | `:8002` | CRUD видео: `GET/POST /api/v1/videos`, `GET/PATCH/DELETE /api/v1/videos/:id`. Внутренний `PATCH /internal/videos/:id/status` для transcoder'а. Валидация, пагинация | `postgres.videos`, `video_renditions` | -| **upload** | Go + `chi` | `:8003` | Принимает `multipart/form-data` на `POST /api/v1/videos/upload`, льёт оригинал в MinIO `raw/{id}/original.mp4`, создаёт запись `status=uploaded` через metadata, публикует `video.uploaded` в RabbitMQ | MinIO + RabbitMQ + metadata | +| **upload** | Go + `chi` | `:8003` | Оркестрирует загрузку оригинала в MinIO `raw/{id}/original.mp4`. Основной путь — S3 multipart (issue #56): `POST /api/v1/videos/multipart` создаёт сессию + запись `status=uploaded` через metadata, `presign-part` отдаёт presigned URL'ы 8МБ чанков (браузер льёт их напрямую в MinIO, ≤10000 частей), `complete` склеивает части, проверяет собранный объект (StatObject) и публикует `video.uploaded` в RabbitMQ. Legacy-пути: `POST /api/v1/videos/upload` (прокси через gateway, fallback для мелких файлов), `POST /presign`+`/complete`, `/resumable` | MinIO + RabbitMQ + metadata | | **transcoder** | Python pika + FFmpeg | — (воркер) | Слушает `video.uploaded`, качает оригинал, `ffprobe` → 3× FFmpeg параллельно (360p/720p/1080p), льёт `renditions/{id}/{quality}.mp4` в MinIO, `PATCH metadata status=ready`, публикует `video.transcoded`. Тут же делает превью (`-ss 1 -vframes 1`) | MinIO, RabbitMQ, metadata | | **streaming (nginx-vod)** | nginx + `kaltura/nginx-vod-module` | internal (без публикации порта, issue #43) | Отдаёт HLS/DASH на лету через gateway `/hls/*` (HLSAuth): `GET /hls/{id}/master.m3u8` склеивает 3 MP4 в мастер-манифест, сегменты режет по ключевым кадрам. JIT — храним только MP4, сегменты не прегенерим. `vod_mode mapped`; MP4 читает из MinIO по presigned GET из mapping (`metadata /internal/videos/:id/vod`) | Читает MinIO (presigned) | -| **frontend** | Next.js 14 + `hls.js` + `zustand` + `tailwind` | `:3000` | Лента `/`, просмотр `/watch/[id]` (`new Hls().loadSource(master.m3u8)` + `playbackRate 0.5–2x`), загрузка `/upload` (multipart + прогресс) | Gateway + HLS | +| **frontend** | Next.js 14 + `hls.js` + `zustand` + `tailwind` | `:3000` | Лента `/`, просмотр `/watch/[id]` (`new Hls().loadSource(master.m3u8)` + `playbackRate 0.5–2x`; для приватного видео сначала hls-token, logout гасит плеер), загрузка `/upload` (S3 multipart чанками 8МБ напрямую в MinIO + resume). API-слой: silent refresh на 401 с guard от гонки ротации между вкладками | Gateway + HLS | | **infra: Postgres** | `postgres:16-alpine` | `:5432` | `users`, `videos (status: uploaded/processing/ready/failed)`, `video_renditions` — см. `deploy/postgres/init.sql:1` | — | | **infra: MinIO** | `minio/minio` | `:9000/:9001` | S3-совместимое хранилище: `raw/` оригиналы, `renditions/` готовые MP4, превью | — | | **infra: RabbitMQ** | `rabbitmq:3-management` | `:5672/:15672` | Очередь `video.uploaded` / `video.transcoded` (брокер pipeline-событий). UI на `:15672` | — | +| **infra: Redis** | `redis` (с паролем, issue #52/#64) | `:6379` | Rate-limit gateway (fixed-window), активный `jti` refresh-токенов auth (`auth:refresh:`) | — | Каждый сервис — свой `Dockerfile` + `go.mod` / `pyproject.toml` (uv), собирается независимо. Локально — `make up` / `docker compose --env-file .env -f deploy/docker-compose.yml up --build -d`. @@ -46,14 +47,16 @@ Flowix — MVP видеоплатформы с адаптивным стрими │ │ JWT в Authorization: Bearer ▼ - ┌─────────┐ POST /api/v1/videos/upload (multipart: file+title) + ┌─────────┐ POST /api/v1/videos/multipart (S3 multipart, issue #56) │ gateway │ ──► upload :8003 └─────────┘ │ ├─1 validate JWT → X-User-ID ├─2 POST metadata /internal/videos → videos.status=uploaded - ├─3 PutObject MinIO raw/{video_id}/original.mp4 - ├─4 Publish RabbitMQ video.uploaded {video_id, s3_key, owner_id} - └─5 201 {id, status: uploaded} → gateway → frontend + ├─3 CreateMultipartUpload raw/{video_id}/original.mp4 + ├─4 presign-part ×N → браузер PUT'ит 8МБ чанки напрямую в MinIO + ├─5 complete: ListParts → CompleteMultipartUpload → StatObject + ├─6 Publish RabbitMQ video.uploaded {video_id, s3_key, owner_id} + └─7 201 {id, status: uploaded} → gateway → frontend │ ▼ ┌──────────┐ RabbitMQ video.uploaded @@ -129,7 +132,7 @@ ffmpeg -i in.mp4 -vn -c:a aac -b:a 128k -ar 48000 -ac 2 audio.m4a 1. `GET /api/v1/videos` → лента. 2. `GET /api/v1/videos/:id` → мета + `status`. -3. Если `ready`, `hls.js` грузит `/hls/{id}/master.m3u8` (через gateway). Внутри `EXT-X-STREAM-INF` на каждое качество. +3. Если `ready`, `hls.js` грузит `/hls/{id}/master.m3u8` (через gateway). Внутри `EXT-X-STREAM-INF` на каждое качество. Приватное видео: сначала `GET /api/v1/videos/{id}/hls-token` (только владелец) → `/hls/{id}/master.m3u8?token=…` (TTL 15м — утечка ссылки закрывается сама); logout обнуляет источник плеера, чтобы подписанный токен не доигрывал в открытой вкладке. 4. Плеер сам выбирает битрейт по сети; ручное переключение — без перезагрузки плеера. `video.playbackRate` для скорости. --- @@ -158,6 +161,7 @@ scripts/e2e.sh — upload→ready→master.m3u8→ffprobe→g - **Загрузка падает**: проверь `upload` логи, MinIO `:9001`, RabbitMQ `:15672` очереди, `metadata` — создалась ли запись `uploaded`. - **Транскодер не берёт задачу**: `make dev-transcoder` логи, `RABBITMQ_URL` в `.env`, FFmpeg установлен (`ffmpeg -version`), права MinIO. - **HLS 404 / нет сегментов**: `curl :8080/hls/{id}/master.m3u8` (gateway, issue #43 — nginx-vod без публикации порта) должен вернуть `EXT-X-STREAM-INF` ×3. Проверь `renditions/` в MinIO, `nginx.conf` `vod_upstream_location` и presign в `metadata /internal/videos/:id/vod`. +- **HLS 403 на приватном видео**: нет/просрочен hls-токен (TTL 15м) или запрос не от владельца — фронт берёт `GET /api/v1/videos/{id}/hls-token` и добавляет `?token=` к URL манифеста. - **Gateway 502**: сервис за ним не поднят — `make ps`, `make logs`. См. также `scripts/e2e.sh` — прогоняет `upload → poll ready → curl master.m3u8 → ffprobe aligned segments (EXTINF cross-check, h264) → gateway /hls` (фаза 8). Prod-пайплайн — `docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.prod.yml up --build -d` (CDN `Cache-Control` для manifests/segments). diff --git a/docs/spec.md b/docs/spec.md index b62d213..fcc55fa 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -24,7 +24,7 @@ The backend is split into microservices written in **Go** and **Python** to leve - **Message Queue**: RabbitMQ or Redis Streams - **Streaming Server**: nginx with `nginx-vod-module` (or custom Go service if preferred) - **Transcoding**: FFmpeg -- **Frontend**: Next + Vite + hls.js (or video.js) +- **Frontend**: Next.js 14 (App Router) + hls.js + zustand - **Containerization**: Docker, docker-compose --- @@ -59,6 +59,7 @@ Each service has its own `Dockerfile` and can be developed independently. Go-с ### 1. API Gateway (Go) - **Purpose**: Single entry point for client requests; routes to appropriate services; handles CORS, rate limiting, request aggregation. - **Security (issue #52)**: CORS — явный allowlist origin (`CORS_ALLOWED_ORIGINS`, без wildcard с credentials); rate-limit — fixed-window в Redis (`REDIS_URL`), fail-open при недоступности Redis; `X-Forwarded-For`/`X-Real-IP` доверяются только от trusted прокси (`TRUSTED_PROXY_CIDRS`), downstream получает проверенный `X-Real-IP` (по нему ключует лимитер auth). +- **HLS (приватные видео)**: `GET /api/v1/videos/{id}/hls-token` выдаёт владельцу короткоживущий (15 мин) подписанный JWT (`type=hls`, привязан к `video_id`); `/hls/*` защищён `HLSAuth` — public/unlisted проходят без токена, private требуют hls-токен или access-токен владельца. Метаданные видео кешируются в gateway (bounded LRU, TTL 10с, негативный 404/403 — 5с, issue #54): один playback даёт 100+ запросов сегментов, каждый не должен бить в metadata. - **Framework**: Gin, Echo, or chi. - **Communication**: REST or gRPC to internal services. @@ -70,14 +71,14 @@ Each service has its own `Dockerfile` and can be developed independently. Go-с ### 3. Upload Service (Go) - **Purpose**: Accept video uploads from users, store raw files in MinIO, publish `video.uploaded` event to queue. - **Framework**: Go standard library or Gin. -- **Key features**: Multipart upload, resumable uploads, progress tracking. +- **Key features**: основной путь — S3 multipart upload (issue #56): браузер PUT'ит presigned 8МБ чанки напрямую в MinIO (≤10000 частей), сервер только создаёт сессию (`POST /api/v1/videos/multipart`), presign'ит части и завершает загрузку (`complete`) с проверкой собранного объекта. Legacy-пути: `POST /api/v1/videos/upload` (прокси через gateway, fallback для мелких файлов), presigned PUT (`/presign` + `/complete`), resumable Content-Range (`/resumable`). ### 4. Auth Service (Python) - **Purpose**: User authentication, JWT issuance, OAuth2 integration. - **Framework**: FastAPI (or Django + DRF). - **Database**: PostgreSQL (users, tokens). - **Communication**: Exposes `/auth/*` endpoints; other services validate JWT via middleware. -- **JWT**: PyJWT (HS256) + argon2-cffi; токены содержат `exp`, `sub`, `type` (`access`/`refresh`); refresh-токены также несут `jti` — активный jti юзера хранится в Redis, ротация в `/refresh` атомарна и отзывает старый refresh (reuse → 401), `/refresh` проверяет существование юзера; размер токена ограничен при decode (issue #48, CVE-2024-33663/33664). +- **JWT**: PyJWT (HS256) + argon2-cffi; токены содержат `exp`, `sub`, `type` (`access`/`refresh`); refresh-токены также несут `jti` — активный jti юзера хранится в Redis, ротация в `/refresh` атомарна и отзывает старый refresh (reuse → 401), `/refresh` проверяет существование юзера; `/me` и Go-мидлвари принимают только `type=access` — refresh не является identity (issue #53, #76); размер токена ограничен при decode (issue #48, CVE-2024-33663/33664). ### 5. Transcoding Worker (Python) - **Purpose**: Consume `video.uploaded` events, download original from MinIO, run FFmpeg to produce multiple renditions, upload results, publish `video.transcoded` event. @@ -128,9 +129,10 @@ docker-compose up --build This will start: - MinIO on `:9000` (API) and `:9001` (console) — published loopback-only (issue #43), bucket fully private: reads via presigned URLs or gateway proxies - PostgreSQL on `:5432` +- Redis on `:6379` — rate-limit gateway, активный jti refresh-токенов auth (issue #64) - RabbitMQ on `:5672` (and management UI on `:15672`) - All microservices -- nginx-vod on `:8080` +- nginx-vod — internal, без публикации порта (issue #43); доступен только через gateway `/hls/*` - Frontend dev server on `:3000` ### Environment Variables