chore: добавить каркас image-platform
- добавлен базовый pnpm workspace для будущих приложений - добавлена dev-инфраструктура PostgreSQL и MinIO - добавлены env-пример и базовые правила репозитория - зафиксированы архитектура, data model и API-контракт - описан контракт с внешним imgproxy
This commit is contained in:
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
30
.env.example
Normal file
30
.env.example
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Local dev infrastructure
|
||||||
|
POSTGRES_DB=image_platform
|
||||||
|
POSTGRES_USER=image
|
||||||
|
POSTGRES_PASSWORD=image-password
|
||||||
|
POSTGRES_PORT=5433
|
||||||
|
DATABASE_URL=postgres://image:image-password@localhost:5433/image_platform
|
||||||
|
|
||||||
|
MINIO_ROOT_USER=image
|
||||||
|
MINIO_ROOT_PASSWORD=image-password
|
||||||
|
MINIO_API_PORT=9000
|
||||||
|
MINIO_CONSOLE_PORT=9001
|
||||||
|
|
||||||
|
S3_ENDPOINT=http://localhost:9000
|
||||||
|
S3_INTERNAL_ENDPOINT=http://minio:9000
|
||||||
|
S3_REGION=us-east-1
|
||||||
|
S3_BUCKET=image-platform
|
||||||
|
S3_ACCESS_KEY_ID=image
|
||||||
|
S3_SECRET_ACCESS_KEY=image-password
|
||||||
|
S3_FORCE_PATH_STYLE=true
|
||||||
|
|
||||||
|
# Future local services
|
||||||
|
PUBLIC_API_BASE_URL=http://localhost:3001
|
||||||
|
PUBLIC_IMAGE_BASE_URL=http://localhost:8888
|
||||||
|
|
||||||
|
# imgproxy is always external for image-platform.
|
||||||
|
# Local example: run imgproxy separately on localhost:18080.
|
||||||
|
IMGPROXY_UPSTREAM=http://localhost:18080
|
||||||
|
IMGPROXY_SIGNING_ENABLED=false
|
||||||
|
IMGPROXY_KEY=
|
||||||
|
IMGPROXY_SALT=
|
||||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.turbo/
|
||||||
|
.next/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Runtime env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# OS / editor
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
60
README.md
Normal file
60
README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Image Platform
|
||||||
|
|
||||||
|
Image Platform - отдельная площадка для управления изображениями, variants, PostgreSQL metadata и S3/Object Storage.
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
Сейчас создан только базовый monorepo и dev-инфраструктура. Приложения `api`, `admin` и `gateway` пока намеренно не созданы.
|
||||||
|
|
||||||
|
## Целевая схема
|
||||||
|
|
||||||
|
```text
|
||||||
|
client
|
||||||
|
-> CDN optional
|
||||||
|
-> gateway Caddy/Souin hot cache
|
||||||
|
-> S3/Object Storage persistent variants
|
||||||
|
-> generator/worker
|
||||||
|
-> external imgproxy
|
||||||
|
-> source/original image
|
||||||
|
```
|
||||||
|
|
||||||
|
`imgproxy` всегда считается внешним сервисом и подключается через `IMGPROXY_UPSTREAM`.
|
||||||
|
|
||||||
|
## Локальная разработка
|
||||||
|
|
||||||
|
В Docker поднимается только базовая инфраструктура:
|
||||||
|
|
||||||
|
- PostgreSQL
|
||||||
|
- MinIO
|
||||||
|
- MinIO bucket init
|
||||||
|
|
||||||
|
Позже нодой будут запускаться:
|
||||||
|
|
||||||
|
- NestJS API
|
||||||
|
- worker
|
||||||
|
- React/Vite admin
|
||||||
|
|
||||||
|
Gateway будет добавлен отдельно позже.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
pnpm install
|
||||||
|
pnpm infra:up
|
||||||
|
pnpm infra:config
|
||||||
|
```
|
||||||
|
|
||||||
|
Порты по умолчанию:
|
||||||
|
|
||||||
|
| Сервис | URL |
|
||||||
|
|---|---|
|
||||||
|
| PostgreSQL | `localhost:5433` |
|
||||||
|
| MinIO API | `http://localhost:9000` |
|
||||||
|
| MinIO Console | `http://localhost:9001` |
|
||||||
|
|
||||||
|
## Документация
|
||||||
|
|
||||||
|
- `docs/architecture.md` - целевая архитектура и ответственность компонентов.
|
||||||
|
- `docs/development.md` - локальный dev flow.
|
||||||
|
- `docs/data-model.md` - черновик PostgreSQL модели.
|
||||||
|
- `docs/api-contract-draft.md` - черновик будущего JSON API.
|
||||||
|
- `docs/imgproxy-contract.md` - контракт с external imgproxy.
|
||||||
124
docs/api-contract-draft.md
Normal file
124
docs/api-contract-draft.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# Черновик API Контракта
|
||||||
|
|
||||||
|
Это не реализация API, а фиксация будущего контракта для NestJS backend.
|
||||||
|
|
||||||
|
Backend отдаёт JSON, metadata, statuses и URLs. Он не должен проксировать image bytes на каждый обычный запрос.
|
||||||
|
|
||||||
|
## Allowed Hosts
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /allowed-hosts
|
||||||
|
POST /allowed-hosts
|
||||||
|
PATCH /allowed-hosts/:id
|
||||||
|
DELETE /allowed-hosts/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /assets
|
||||||
|
POST /assets
|
||||||
|
GET /assets/:id
|
||||||
|
DELETE /assets/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
`POST /assets` request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sourceUrl": "https://example.com/photo.jpg"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- validate source URL;
|
||||||
|
- check `allowed_image_hosts`;
|
||||||
|
- create or reuse `image_assets` row;
|
||||||
|
- optionally save original to S3 later.
|
||||||
|
|
||||||
|
## Variants
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /assets/:id/variants
|
||||||
|
POST /assets/:id/variants
|
||||||
|
POST /variants/:id/regenerate
|
||||||
|
DELETE /variants/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
`POST /assets/:id/variants` request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"preset": "card",
|
||||||
|
"format": "webp",
|
||||||
|
"width": 640
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response if ready:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "variant_123",
|
||||||
|
"status": "ready",
|
||||||
|
"url": "http://localhost:8888/images/asset_123/w640_q80_card.webp"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response if generation is async:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "variant_123",
|
||||||
|
"status": "pending",
|
||||||
|
"url": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image URLs For UI
|
||||||
|
|
||||||
|
Для UI нужен endpoint, который возвращает готовый набор URLs для `<picture>`/`srcset`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /assets/:id/picture?preset=card
|
||||||
|
```
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"assetId": "asset_123",
|
||||||
|
"preset": "card",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "image/avif",
|
||||||
|
"srcset": "http://localhost:8888/images/asset_123/w320_card.avif 320w, http://localhost:8888/images/asset_123/w640_card.avif 640w"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image/webp",
|
||||||
|
"srcset": "http://localhost:8888/images/asset_123/w320_card.webp 320w, http://localhost:8888/images/asset_123/w640_card.webp 640w"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fallback": {
|
||||||
|
"src": "http://localhost:8888/images/asset_123/w640_card.jpg",
|
||||||
|
"width": 640,
|
||||||
|
"height": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Worker Lifecycle
|
||||||
|
|
||||||
|
Первый MVP может генерировать sync на request. Если генерация тяжёлая, variant создаётся как `pending`, а worker обрабатывает job.
|
||||||
|
|
||||||
|
PostgreSQL может выступить первой очередью:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SELECT * FROM image_variants
|
||||||
|
WHERE status = 'pending'
|
||||||
|
FOR UPDATE SKIP LOCKED
|
||||||
|
LIMIT 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Позже можно добавить Redis/Valkey или отдельную queue, если PostgreSQL станет узким местом.
|
||||||
84
docs/architecture.md
Normal file
84
docs/architecture.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Архитектура
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
`image-platform` - отдельный control plane для своего Cloudinary-like image pipeline.
|
||||||
|
|
||||||
|
Проект отвечает за metadata, S3 artifacts, variants, allowlist, presets и генерацию изображений через внешний `imgproxy`.
|
||||||
|
|
||||||
|
## Компоненты
|
||||||
|
|
||||||
|
| Компонент | Статус | Роль |
|
||||||
|
|---|---|---|
|
||||||
|
| PostgreSQL | сейчас | Источник правды для assets, variants, hosts, statuses |
|
||||||
|
| S3/MinIO | сейчас | Хранилище originals и generated variants |
|
||||||
|
| API | позже | JSON API, admin operations, validation, orchestration |
|
||||||
|
| Worker | позже | Генерация variants, upload в S3, update PostgreSQL |
|
||||||
|
| Admin UI | позже | Управление hosts/assets/variants/presets |
|
||||||
|
| Gateway | позже | Caddy/Souin hot cache и delivery layer |
|
||||||
|
| imgproxy | external | CPU-heavy image processing |
|
||||||
|
|
||||||
|
## Целевая delivery схема
|
||||||
|
|
||||||
|
```text
|
||||||
|
client
|
||||||
|
-> CDN optional
|
||||||
|
-> gateway Caddy/Souin
|
||||||
|
-> S3 ready variant
|
||||||
|
-> generator fallback
|
||||||
|
-> external imgproxy
|
||||||
|
-> source image
|
||||||
|
```
|
||||||
|
|
||||||
|
## Разделение ответственности
|
||||||
|
|
||||||
|
PostgreSQL отвечает на вопросы:
|
||||||
|
|
||||||
|
- какие assets зарегистрированы;
|
||||||
|
- какие variants созданы;
|
||||||
|
- где variants лежат в S3;
|
||||||
|
- какие variants `pending`, `processing`, `ready`, `failed`;
|
||||||
|
- сколько bytes занимает asset/project/user;
|
||||||
|
- какие source hosts разрешены.
|
||||||
|
|
||||||
|
S3 хранит байты:
|
||||||
|
|
||||||
|
- original images, если решим сохранять originals;
|
||||||
|
- generated variants;
|
||||||
|
- metadata объектов на уровне storage, но не бизнес-логику.
|
||||||
|
|
||||||
|
Gateway отдаёт картинки:
|
||||||
|
|
||||||
|
- hot cache HIT - сразу из Souin;
|
||||||
|
- cache MISS - из S3;
|
||||||
|
- S3 MISS - через generator fallback.
|
||||||
|
|
||||||
|
Backend не должен проксировать картинки на каждый обычный запрос. Он отдаёт JSON, статусы и URLs.
|
||||||
|
|
||||||
|
## URL модель
|
||||||
|
|
||||||
|
Публичные URL должны быть стабильными и не раскрывать source URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/images/{assetId}/{variantHash}.{format}
|
||||||
|
```
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/images/asset_123/w640_q80_cfill.avif
|
||||||
|
/images/asset_123/w640_q80_cfill.webp
|
||||||
|
/images/asset_123/w640_q80_cfill.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Формат лучше делать явным в URL и отдавать через `<picture>`/`srcset`, а не выбирать по `Accept` header. Так CDN/S3/Gateway cache остаётся предсказуемым.
|
||||||
|
|
||||||
|
## External imgproxy
|
||||||
|
|
||||||
|
`imgproxy` не входит в этот проект и не деплоится вместе с ним. Он подключается через env:
|
||||||
|
|
||||||
|
```env
|
||||||
|
IMGPROXY_UPSTREAM=http://external-imgproxy.internal:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Это позволяет держать image processing на отдельной мощной машине и не рисковать основным сервером.
|
||||||
102
docs/data-model.md
Normal file
102
docs/data-model.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Черновик Data Model
|
||||||
|
|
||||||
|
Это черновик для будущих миграций PostgreSQL. Реальные таблицы добавим вместе с API.
|
||||||
|
|
||||||
|
## allowed_image_hosts
|
||||||
|
|
||||||
|
```text
|
||||||
|
id
|
||||||
|
hostname
|
||||||
|
enabled
|
||||||
|
description nullable
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила normalization:
|
||||||
|
|
||||||
|
- lowercase;
|
||||||
|
- без protocol;
|
||||||
|
- без path;
|
||||||
|
- без trailing slash;
|
||||||
|
- без wildcard на первом этапе;
|
||||||
|
- source URL должен быть `http` или `https`;
|
||||||
|
- запрещены localhost, private IP, loopback, link-local.
|
||||||
|
|
||||||
|
## image_assets
|
||||||
|
|
||||||
|
```text
|
||||||
|
id
|
||||||
|
source_url
|
||||||
|
source_host
|
||||||
|
source_hash
|
||||||
|
original_s3_key nullable
|
||||||
|
status
|
||||||
|
width nullable
|
||||||
|
height nullable
|
||||||
|
content_type nullable
|
||||||
|
size_bytes nullable
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
## image_variants
|
||||||
|
|
||||||
|
```text
|
||||||
|
id
|
||||||
|
asset_id
|
||||||
|
preset
|
||||||
|
variant_hash
|
||||||
|
format
|
||||||
|
width
|
||||||
|
height nullable
|
||||||
|
quality
|
||||||
|
s3_key
|
||||||
|
status: pending | processing | ready | failed
|
||||||
|
size_bytes nullable
|
||||||
|
error nullable
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
last_accessed_at nullable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unique constraints
|
||||||
|
|
||||||
|
```text
|
||||||
|
allowed_image_hosts(hostname)
|
||||||
|
image_assets(source_hash)
|
||||||
|
image_variants(asset_id, variant_hash, format)
|
||||||
|
```
|
||||||
|
|
||||||
|
## S3 layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
originals/{assetId}/source
|
||||||
|
variants/{assetId}/{variantHash}.{format}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Presets
|
||||||
|
|
||||||
|
Клиент не должен передавать произвольные трансформации. Сначала нужны ограниченные presets.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```text
|
||||||
|
avatar:
|
||||||
|
widths: 128, 256, 512
|
||||||
|
formats: avif, webp, jpg
|
||||||
|
quality: 80
|
||||||
|
resize: fill
|
||||||
|
|
||||||
|
card:
|
||||||
|
widths: 320, 640, 960
|
||||||
|
formats: avif, webp, jpg
|
||||||
|
quality: 80
|
||||||
|
resize: fit
|
||||||
|
|
||||||
|
hero:
|
||||||
|
widths: 1280, 1920
|
||||||
|
formats: avif, webp, jpg
|
||||||
|
quality: 80
|
||||||
|
resize: fit
|
||||||
|
```
|
||||||
84
docs/development.md
Normal file
84
docs/development.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Локальная разработка
|
||||||
|
|
||||||
|
## Принцип
|
||||||
|
|
||||||
|
В Docker запускаем стабильную инфраструктуру. Кодовые сервисы позже запускаем нодой с hot reload.
|
||||||
|
|
||||||
|
Сейчас в Docker есть только:
|
||||||
|
|
||||||
|
- PostgreSQL;
|
||||||
|
- MinIO;
|
||||||
|
- MinIO bucket init.
|
||||||
|
|
||||||
|
`api`, `worker`, `admin` и `gateway` пока не созданы.
|
||||||
|
|
||||||
|
## Запуск инфраструктуры
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
pnpm install
|
||||||
|
pnpm infra:up
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверить compose config:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm infra:config
|
||||||
|
```
|
||||||
|
|
||||||
|
Остановить:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm infra:down
|
||||||
|
```
|
||||||
|
|
||||||
|
Логи:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm infra:logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Порты
|
||||||
|
|
||||||
|
| Сервис | URL |
|
||||||
|
|---|---|
|
||||||
|
| PostgreSQL | `localhost:5433` |
|
||||||
|
| MinIO API | `http://localhost:9000` |
|
||||||
|
| MinIO Console | `http://localhost:9001` |
|
||||||
|
|
||||||
|
## Будущий dev flow
|
||||||
|
|
||||||
|
Когда появятся приложения:
|
||||||
|
|
||||||
|
```text
|
||||||
|
React/Vite admin localhost:5173
|
||||||
|
-> NestJS API localhost:3001
|
||||||
|
-> PostgreSQL localhost:5433
|
||||||
|
-> MinIO localhost:9000
|
||||||
|
|
||||||
|
worker node process
|
||||||
|
-> PostgreSQL
|
||||||
|
-> MinIO
|
||||||
|
-> external imgproxy
|
||||||
|
|
||||||
|
gateway Caddy/Souin localhost:8888
|
||||||
|
-> S3/MinIO ready variant
|
||||||
|
-> API/generator fallback on host.docker.internal:3001
|
||||||
|
```
|
||||||
|
|
||||||
|
Для Linux gateway container должен видеть host services через:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
```
|
||||||
|
|
||||||
|
## External imgproxy для разработки
|
||||||
|
|
||||||
|
`imgproxy` не входит в `image-platform` stack. Для локальной разработки можно использовать любой внешний endpoint и прописать его в `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
IMGPROXY_UPSTREAM=http://localhost:18080
|
||||||
|
```
|
||||||
|
|
||||||
|
Если нужен локальный standalone imgproxy, его можно запустить отдельно вне этого compose stack. Он остаётся внешней зависимостью платформы.
|
||||||
71
docs/imgproxy-contract.md
Normal file
71
docs/imgproxy-contract.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Контракт с imgproxy
|
||||||
|
|
||||||
|
`imgproxy` всегда внешний сервис для `image-platform`.
|
||||||
|
|
||||||
|
## Env
|
||||||
|
|
||||||
|
```env
|
||||||
|
IMGPROXY_UPSTREAM=http://external-imgproxy.internal:8080
|
||||||
|
IMGPROXY_SIGNING_ENABLED=false
|
||||||
|
IMGPROXY_KEY=
|
||||||
|
IMGPROXY_SALT=
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dev режим
|
||||||
|
|
||||||
|
В dev можно использовать unsigned `/unsafe` URL, если внешний `imgproxy` запущен без key/salt.
|
||||||
|
|
||||||
|
Пример path:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/unsafe/resize:fit:800:0:0/q:80/plain/https://example.com/photo.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prod режим
|
||||||
|
|
||||||
|
В prod нужно перейти на signed URLs и закрыть `/unsafe`.
|
||||||
|
|
||||||
|
Path для подписи строится без `/unsafe`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/resize:fit:800:0:0/q:80/plain/https://example.com/photo.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Signature:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HMAC-SHA256(binary_key, binary_salt + path_bytes)
|
||||||
|
base64url without padding
|
||||||
|
```
|
||||||
|
|
||||||
|
Node implementation reference:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import crypto from "node:crypto"
|
||||||
|
|
||||||
|
export function signImgproxyPath(keyHex: string, saltHex: string, path: string) {
|
||||||
|
const key = Buffer.from(keyHex, "hex")
|
||||||
|
const salt = Buffer.from(saltHex, "hex")
|
||||||
|
const hmac = crypto.createHmac("sha256", key)
|
||||||
|
|
||||||
|
hmac.update(Buffer.concat([salt, Buffer.from(path)]))
|
||||||
|
|
||||||
|
return hmac.digest("base64url")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Final signed URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{IMGPROXY_UPSTREAM}/{signature}{path}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security rules
|
||||||
|
|
||||||
|
- Не отдавать `IMGPROXY_KEY` и `IMGPROXY_SALT` в браузер.
|
||||||
|
- Source URL валидировать в API/worker.
|
||||||
|
- Разрешать только `http` и `https`.
|
||||||
|
- Запрещать localhost, private IP, loopback, link-local.
|
||||||
|
- Source host должен быть enabled в `allowed_image_hosts`.
|
||||||
|
- Не давать клиенту произвольные imgproxy options.
|
||||||
|
- Использовать presets и deterministic `variantHash`.
|
||||||
51
infra/compose.dev.yml
Normal file
51
infra/compose.dev.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: image-platform
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-image_platform}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-image}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-image-password}
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_PORT:-5433}:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-image}
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-image-password}
|
||||||
|
ports:
|
||||||
|
- "${MINIO_API_PORT:-9000}:9000"
|
||||||
|
- "${MINIO_CONSOLE_PORT:-9001}:9001"
|
||||||
|
volumes:
|
||||||
|
- minio-data:/data
|
||||||
|
|
||||||
|
minio-init:
|
||||||
|
image: minio/mc:latest
|
||||||
|
depends_on:
|
||||||
|
- minio
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-image}
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-image-password}
|
||||||
|
S3_BUCKET: ${S3_BUCKET:-image-platform}
|
||||||
|
entrypoint: ["/bin/sh", "-c"]
|
||||||
|
command: >
|
||||||
|
"until mc alias set local http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}; do sleep 1; done &&
|
||||||
|
mc mb --ignore-existing local/$${S3_BUCKET} &&
|
||||||
|
mc anonymous set download local/$${S3_BUCKET}"
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
minio-data:
|
||||||
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "image-platform",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Control plane for image assets, variants, S3 storage and imgproxy generation.",
|
||||||
|
"packageManager": "pnpm@10.28.1",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.0.0",
|
||||||
|
"pnpm": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"infra:config": "docker compose -f infra/compose.dev.yml config",
|
||||||
|
"infra:up": "docker compose -f infra/compose.dev.yml up -d",
|
||||||
|
"infra:down": "docker compose -f infra/compose.dev.yml down",
|
||||||
|
"infra:logs": "docker compose -f infra/compose.dev.yml logs -f",
|
||||||
|
"check": "pnpm infra:config"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
pnpm-lock.yaml
generated
Normal file
9
pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.: {}
|
||||||
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
packages:
|
||||||
|
- "apps/*"
|
||||||
|
- "packages/*"
|
||||||
Reference in New Issue
Block a user