chore(openspec): archive shared-album and sync main specs
Move the completed shared-album change to openspec/changes/archive/2026-10-10-shared-album/ and create the 8 main specs (albums, auth, members, notifications, photos, realtime, share-links, social) from the delta ADDED requirements.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
Greenfield-сервис «многопользовательский фотоальбом». Существующая инфраструктура:
|
||||
PostgreSQL (`pg.localnet:5432`) и Garage S3 (`s3.localnet`, path-style, регион `garage`,
|
||||
ключ `GK...`). Жёсткое ограничение — без Docker: один статичный Go-бинарник со вшитой
|
||||
статикой SvelteKit через `go:embed`, запуск через systemd. Мотивация — в proposal.md.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Один артефакт (Go-бинарник), `CGO_ENABLED=0`, self-hosted поверх Garage + PostgreSQL.
|
||||
- Чёткая модель прав, проверяемая на уровне сервиса, а не только UI.
|
||||
- Live-обновления через SSE без внешних зависимостей.
|
||||
|
||||
**Non-Goals:**
|
||||
- Горизонтальное масштабирование (один инстанс, in-memory SSE-хаб).
|
||||
- Web push (браузерные push) — отложено; уведомления только in-app через SSE.
|
||||
- Публичный каталог альбомов — доступ вне состава только по share-ссылке.
|
||||
- Zip-скачивание альбомов.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Обработка изображений: `imaging` + `gen2brain/webp` + `goexif`
|
||||
Resize — `disintegration/imaging`, WebP-кодирование — `gen2brain/webp` (libwebp,
|
||||
транспилированный в чистый Go/WASM; `kolesa-team/go-webp` отклонён, т.к. это cgo-биндинг,
|
||||
несовместимый с `CGO_ENABLED=0`), EXIF — `rwcarlsen/goexif`. Всё чистый Go, совместимо с `CGO_ENABLED=0`.
|
||||
- **Альтернатива (отклонена):** `bimg`/libvips — быстрее и нативный WebP/EXIF, но требует
|
||||
CGO и libvips на сборочной и прод-машинах, ломает «статичный бинарник».
|
||||
- **Альтернатива (запасная):** `imaging` + JPEG-превью — проще, но превью тяжелее ~25–35%.
|
||||
- Конвейер: stream original в S3 → EXIF+resize → encode WebP → thumb/preview в S3.
|
||||
|
||||
### 2. Аутентификация: JWT access + refresh с хранением refresh-токена
|
||||
Access 15 мин, refresh 30 дней. Refresh-токены хранятся в отдельной таблице
|
||||
`refresh_tokens` (хэшированными) — это даёт отзыв при logout и ротацию при refresh.
|
||||
- **Альтернатива:** stateless refresh-JWT без хранения — проще, но logout не может
|
||||
отозвать токен (противоречит спец `auth`).
|
||||
- SSE: `EventSource` не умеет заголовки, поэтому access-токен в `?token=`; короткий TTL
|
||||
компенсирует риск. (Cookie+CSRF — отложено.)
|
||||
|
||||
### 3. Права: «нет эскалации», владелец из `albums.owner_id`
|
||||
Роли `admin/editor/member/viewer` хранятся в `album_members`. Владелец — создатель,
|
||||
выводится из `albums.owner_id`, не дублируется в составе. Правило: никто не может выдать
|
||||
роль выше своей; админ управляет `editor/member/viewer`, владелец — админами; редактор
|
||||
приглашает только `member/viewer`. Проверка на уровне сервиса.
|
||||
|
||||
### 4. Скачивание: только одиночное фото
|
||||
`GET /api/photos/:id/download` — presigned URL оригинала. Zip альбома исключён из MVP.
|
||||
|
||||
### 5. Доступ по ссылке: таблица `share_links` вместо `privacy`
|
||||
`privacy` на `albums` убран. `share_links` (id, album_id, token, created_by, created_at,
|
||||
expires_at, revoked_at) — несколько ссылок, срок при создании, точечный отзыв. Доступ по
|
||||
токену — view-only (сетка, лайтбокс, одиночное скачивание), без комментариев/лайков/загрузки.
|
||||
Публичные эндпоинты `/api/share/:token/*` отдают свежие presigned URL.
|
||||
|
||||
### 6. Presigned URL: TTL 1 час + выдача по требованию
|
||||
Клиент не хранит URL надолго: сетка получает свежие thumb/preview при каждом запросе списка,
|
||||
лайтбокс дёргает `GET /api/photos/:id` перед показом оригинала. SSE-событие несёт `thumb_url`
|
||||
только для мгновенного рендера. Короткий TTL сохраняет малое окно утечки для приватных фото.
|
||||
|
||||
### 7. SSE: in-memory hub на каналах
|
||||
`map[albumID]map[clientID]chan Event`, неблокирующая публикация, отключение при переполнении
|
||||
буфера, heartbeat 25 сек, отдельная горутина на клиента. Догон пропущенного — REST
|
||||
(`photos?since=`). Один инстанс (см. Non-Goals); на рестарте клиенты переподключаются.
|
||||
|
||||
### 8. Фреймворк и структура
|
||||
Backend — Gin + pgx + AWS SDK v2 (path-style, регион `garage`). Frontend — SvelteKit 2 +
|
||||
Svelte 5, adapter-static, статика в `backend/static` через `go:embed`. Миграции —
|
||||
golang-migrate. Логи — slog. Конфиг — env (12-factor).
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Чистый Go медленнее libvips]** → Приемлемо для MVP; превью генерируются асинхронно,
|
||||
не в пути запроса. При росте нагрузки — миграция на bimg с отказом от `CGO_ENABLED=0`.
|
||||
- **[In-memory SSE-хаб теряет события при рестарте]** → Клиенты переподключаются и догоняют
|
||||
через REST; окно потери ограничено.
|
||||
- **[Токен в query-параметре]** → Короткий TTL access-токена и refresh в httpOnly-хранилище
|
||||
на фронте; Cookie+CSRF как дальнейшее улучшение.
|
||||
- **[Presigned URL в 1 час]** → Может «протухать» на долгоживущих вкладках → компенсируется
|
||||
выдачей по требованию (лайтбокс, свежий список).
|
||||
- **[Несколько share-ссылок]** → Небольшой оверхед на управление, но даёт точечный отзыв
|
||||
(важно для сценария «фотограф ↔ несколько клиентов»).
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Greenfield: golang-migrate применяет `up` к `pg.localnet` при деплое. Таблицы: users,
|
||||
albums (без `privacy`), album_members, invites, photos, comments, likes, notifications,
|
||||
share_links, refresh_tokens. Откат — `down`-миграции или переустановка предыдущего бинарника.
|
||||
Reference in New Issue
Block a user