feat: implement shared-album multi-user photo album
Backend (Go/Gin/pgx): env config, pgx pool with retries, Garage S3 client (path-style, region garage), embedded golang-migrate migrations, JWT auth with refresh rotation, albums CRUD, members/roles/invites with no-escalation matrix, photo upload + async imaging/goexif WebP variants, cursor pagination, presigned URLs, likes/comments, SSE hub (album + personal), notifications, share links. Frontend (SvelteKit 2 + Svelte 5 + Tailwind v4): API wrapper with 401 refresh, auth store, all screens, Uploader/PhotoGrid/Lightbox/Comments, SSE client with reconnect + REST catch-up, three themes + system via data-theme. Deploy without Docker: Makefile builds the frontend into backend/static and embeds it (go:embed) into a single statically linked binary; systemd unit and nginx SSE example included. Use gen2brain/webp (-tags nodynamic) instead of cgo kolesa-team/go-webp to keep CGO_ENABLED=0 static builds; design.md/proposal.md updated accordingly.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-10
|
||||
@@ -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`-миграции или переустановка предыдущего бинарника.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Фото участников разбросаны по мессенджерам и галереям — теряется авторство, контекст
|
||||
и разграничение прав. Нужно единое многопользовательское пространство (семьи и друзья,
|
||||
рабочие команды, фотограф ↔ клиент) с понятными ролями, live-обновлениями и self-hosted
|
||||
хранением поверх уже существующих Garage (S3) и PostgreSQL, без контейнеров.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Новый backend на Go 1.22+ (Gin, pgx, AWS SDK v2, нативный SSE) — один статичный
|
||||
бинарник (`CGO_ENABLED=0`) со вшитой статикой SvelteKit через `go:embed`.
|
||||
- Новый frontend на SvelteKit 2 + Svelte 5 (adapter-static, TypeScript, TailwindCSS).
|
||||
- Аутентификация: регистрация, вход, JWT (access 15 мин + refresh 30 дней), logout.
|
||||
- Альбомы: создание / редактирование / удаление / список, владение альбомом.
|
||||
- Участники и роли: инвайт по токену, роли `admin` / `editor` / `member` / `viewer`,
|
||||
владелец выводится из `albums.owner_id`, правило «нет эскалации прав».
|
||||
- Фото: загрузка (одиночно и массово), три варианта (thumb 200px / preview 1200px /
|
||||
original), WebP-превью, EXIF, cursor-пагинация, просмотр, скачивание одиночного фото.
|
||||
- Социальное: лайки и комментарии к фото.
|
||||
- Real-time через SSE: события фото / комментариев / лайков / участников.
|
||||
- Уведомления: персональный поток в приложении (in-app через SSE).
|
||||
- Ссылки для просмотра: view-only доступ к альбому без аккаунта (несколько ссылок,
|
||||
срок действия, отзыв).
|
||||
- Деплой без Docker: systemd-юнит, статика вшита в бинарник, один артефакт.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `auth`: регистрация и вход по email/паролю, JWT-токены, refresh, logout.
|
||||
- `albums`: жизненный цикл альбома (создание, просмотр, редактирование, удаление,
|
||||
список) и владение им.
|
||||
- `members`: состав альбома, роли и права доступа, инвайт по токену, смена ролей.
|
||||
- `photos`: загрузка и хранение фото в S3, генерация вариантов и EXIF, листинг с
|
||||
пагинацией, просмотр, скачивание одиночного фото.
|
||||
- `social`: лайки и комментарии к фотографиям.
|
||||
- `realtime`: поток событий альбома через SSE (фото, комментарии, лайки, участники).
|
||||
- `notifications`: персональные уведомления пользователя и их статус прочитано.
|
||||
- `share-links`: view-only доступ к альбому по ссылке без авторизации.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_Нет — проект новый, существующих спец нет._
|
||||
|
||||
## Impact
|
||||
|
||||
- **Код:** новый `backend/` (Go) и `frontend/` (SvelteKit), `deploy/` (systemd, nginx),
|
||||
`Makefile`, `.env.example`.
|
||||
- **Зависимости:** `gin`, `pgx`, `aws-sdk-go-v2`, `golang-migrate`, `imaging`,
|
||||
`gen2brain/webp` (чистый Go), `rwcarlsen/goexif`, `golang-jwt`, `bcrypt`.
|
||||
- **Системы:** подключение к существующим `pg.localnet:5432` (PostgreSQL) и
|
||||
Garage на `s3.localnet` (path-style, регион `garage`, ключ `GK...`).
|
||||
- **Права:** все эндпоинты за JWT, кроме `/api/auth/*` и публичных share-эндпоинтов;
|
||||
проверка прав на уровне сервиса, не только UI.
|
||||
- **Не входит в MVP (осознанно отложено):** web push (браузерные push-уведомления),
|
||||
распознавание лиц/ML-теги, мобильные приложения, биллинг, публичный API, печать.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Управление фотоальбомами: создание, просмотр, редактирование, удаление и владение.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Создание альбома
|
||||
Зарегистрированный пользователь SHALL иметь возможность создать альбом и стать его владельцем.
|
||||
|
||||
#### Scenario: Создание альбома
|
||||
- **WHEN** пользователь создаёт альбом с названием
|
||||
- **THEN** система создаёт альбом и назначает пользователя владельцем
|
||||
|
||||
### Requirement: Список альбомов пользователя
|
||||
Пользователь SHALL иметь возможность получить список альбомов, к которым у него есть доступ.
|
||||
|
||||
#### Scenario: Список альбомов
|
||||
- **WHEN** пользователь запрашивает список альбомов
|
||||
- **THEN** система возвращает альбомы, где он владелец или участник
|
||||
|
||||
### Requirement: Получение альбома
|
||||
Участник SHALL иметь возможность получить данные альбома по id.
|
||||
|
||||
#### Scenario: Просмотр альбома
|
||||
- **WHEN** участник запрашивает альбом по id
|
||||
- **THEN** система возвращает данные альбома
|
||||
|
||||
### Requirement: Редактирование альбома
|
||||
Владелец и админ SHALL иметь возможность редактировать название, описание и обложку альбома.
|
||||
|
||||
#### Scenario: Редактирование альбома
|
||||
- **WHEN** админ изменяет описание альбома
|
||||
- **THEN** система сохраняет изменения
|
||||
|
||||
### Requirement: Удаление альбома
|
||||
Владелец SHALL иметь возможность удалить альбом вместе с содержимым.
|
||||
|
||||
#### Scenario: Удаление альбома
|
||||
- **WHEN** владелец удаляет альбом
|
||||
- **THEN** система удаляет альбом, его фото, участников и share-ссылки
|
||||
@@ -0,0 +1,47 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Регистрация и аутентификация пользователей по email и паролю с выдачей краткоживущих и долгоживущих JWT-токенов.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Регистрация пользователя
|
||||
Пользователь SHALL иметь возможность зарегистрироваться, указав email, пароль и имя. Пароль SHALL храниться в виде bcrypt-хэша.
|
||||
|
||||
#### Scenario: Успешная регистрация
|
||||
- **WHEN** пользователь отправляет уникальный email, пароль и имя
|
||||
- **THEN** система создаёт учётную запись и возвращает access- и refresh-токены
|
||||
|
||||
#### Scenario: Повторный email
|
||||
- **WHEN** пользователь регистрируется с уже занятым email
|
||||
- **THEN** система возвращает ошибку конфликта и не создаёт вторую запись
|
||||
|
||||
### Requirement: Вход по email и паролю
|
||||
Пользователь SHALL иметь возможность войти, указав email и пароль.
|
||||
|
||||
#### Scenario: Успешный вход
|
||||
- **WHEN** пользователь вводит верные email и пароль
|
||||
- **THEN** система возвращает access- и refresh-токены
|
||||
|
||||
#### Scenario: Неверные учётные данные
|
||||
- **WHEN** пользователь вводит неверный email или пароль
|
||||
- **THEN** система возвращает 401, не раскрывая, какое именно поле неверно
|
||||
|
||||
### Requirement: Обновление access-токена
|
||||
Пользователь SHALL иметь возможность обменять refresh-токен на новую пару токенов. Access-токен живёт 15 минут, refresh-токен — 30 дней.
|
||||
|
||||
#### Scenario: Валидный refresh-токен
|
||||
- **WHEN** пользователь предоставляет действующий refresh-токен
|
||||
- **THEN** система выдаёт новый access-токен и ротирует refresh-токен
|
||||
|
||||
#### Scenario: Недействительный refresh-токен
|
||||
- **WHEN** пользователь предоставляет истёкший или отозванный refresh-токен
|
||||
- **THEN** система возвращает 401
|
||||
|
||||
### Requirement: Выход из системы
|
||||
Пользователь SHALL иметь возможность выйти, отозвав refresh-токен.
|
||||
|
||||
#### Scenario: Выход
|
||||
- **WHEN** пользователь вызывает logout с действующим refresh-токеном
|
||||
- **THEN** система отзывает refresh-токен, и его дальнейшее использование отклоняется
|
||||
@@ -0,0 +1,65 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Управление составом альбома, ролями и правами доступа, включая приглашения по токену.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Роли участников
|
||||
Участник альбома SHALL иметь одну из ролей: admin, editor, member, viewer. Владелец выводится из albums.owner_id и не хранится как роль участника.
|
||||
|
||||
#### Scenario: Создатель становится владельцем
|
||||
- **WHEN** пользователь создаёт альбом
|
||||
- **THEN** система делает его владельцем альбома
|
||||
|
||||
### Requirement: Матрица прав
|
||||
Система SHALL ограничивать действия по ролям: загружать фото могут owner/admin/editor/member; удалять чужие фото — owner/admin; удалять свои — owner/admin/editor/member; комментировать — owner/admin/editor/member; скачивать одиночное фото и просматривать — все участники, включая viewer.
|
||||
|
||||
#### Scenario: Наблюдатель не может загружать
|
||||
- **WHEN** участник с ролью viewer пытается загрузить фото
|
||||
- **THEN** система отклоняет запрос
|
||||
|
||||
#### Scenario: Участник не может удалять чужие фото
|
||||
- **WHEN** участник с ролью member пытается удалить фото другого автора
|
||||
- **THEN** система отклоняет запрос
|
||||
|
||||
### Requirement: Приглашение участников
|
||||
Владелец и админ SHALL иметь возможность создавать инвайт-ссылку с токеном. Редактор SHALL иметь возможность приглашать только на роли member и viewer. Никто не может приглашать на роль выше своей.
|
||||
|
||||
#### Scenario: Админ приглашает участника
|
||||
- **WHEN** админ создаёт инвайт на роль member
|
||||
- **THEN** система создаёт инвайт с токеном и сроком действия
|
||||
|
||||
#### Scenario: Редактор не может приглашать админа
|
||||
- **WHEN** редактор пытается создать инвайт на роль admin или editor
|
||||
- **THEN** система отклоняет запрос
|
||||
|
||||
### Requirement: Принятие инвайта
|
||||
Пользователь SHALL иметь возможность стать участником, перейдя по токену инвайта.
|
||||
|
||||
#### Scenario: Принятие действующего инвайта
|
||||
- **WHEN** пользователь принимает неистёкший инвайт с неисчерпанным лимитом использования
|
||||
- **THEN** система добавляет его в состав альбома с ролью из инвайта
|
||||
|
||||
#### Scenario: Принятие истёкшего инвайта
|
||||
- **WHEN** пользователь принимает истёкший инвайт
|
||||
- **THEN** система отклоняет запрос
|
||||
|
||||
### Requirement: Смена ролей
|
||||
Админ SHALL иметь возможность менять роли editor/member/viewer. Владелец SHALL управлять ролями админов. Никто не может менять роль выше своей и не может трогать владельца.
|
||||
|
||||
#### Scenario: Админ меняет роль участника
|
||||
- **WHEN** админ повышает участника до editor
|
||||
- **THEN** система обновляет роль
|
||||
|
||||
#### Scenario: Админ не может назначить админа
|
||||
- **WHEN** админ пытается назначить другому участнику роль admin
|
||||
- **THEN** система отклоняет запрос
|
||||
|
||||
### Requirement: Удаление участника
|
||||
Владелец и админ SHALL иметь возможность удалять участников, кроме владельца и других админов.
|
||||
|
||||
#### Scenario: Удаление участника
|
||||
- **WHEN** админ удаляет участника с ролью member/viewer/editor
|
||||
- **THEN** система убирает его из состава альбома
|
||||
@@ -0,0 +1,28 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Персональные уведомления пользователя о событиях (комментарии, лайки, участники) и их статус прочитано.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Создание уведомления
|
||||
Система SHALL создавать уведомление пользователю при наступлении события (комментарий к его фото, лайк, добавление в альбом).
|
||||
|
||||
#### Scenario: Комментарий к моему фото
|
||||
- **WHEN** другой пользователь комментирует фото пользователя
|
||||
- **THEN** система создаёт уведомление этому пользователю
|
||||
|
||||
### Requirement: Список уведомлений
|
||||
Пользователь SHALL иметь возможность получить свои уведомления.
|
||||
|
||||
#### Scenario: Получение уведомлений
|
||||
- **WHEN** пользователь запрашивает уведомления
|
||||
- **THEN** система возвращает список с признаком прочитано
|
||||
|
||||
### Requirement: Отметка прочитанным
|
||||
Пользователь SHALL иметь возможность отметить уведомление прочитанным.
|
||||
|
||||
#### Scenario: Отметить прочитанным
|
||||
- **WHEN** пользователь отмечает уведомление прочитанным
|
||||
- **THEN** система фиксирует время прочтения
|
||||
@@ -0,0 +1,57 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Загрузка, хранение в S3 и просмотр фотографий с генерацией вариантов и извлечением EXIF.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Загрузка фото
|
||||
Пользователь с правом загрузки SHALL иметь возможность загружать фото по одному и массово.
|
||||
|
||||
#### Scenario: Успешная загрузка
|
||||
- **WHEN** участник с правом загрузки отправляет файл изображения
|
||||
- **THEN** система сохраняет оригинал и создаёт запись фото с метаданными
|
||||
|
||||
#### Scenario: Загрузка без права
|
||||
- **WHEN** наблюдатель пытается загрузить фото
|
||||
- **THEN** система отклоняет запрос
|
||||
|
||||
### Requirement: Варианты изображений
|
||||
Система SHALL генерировать три варианта: thumb (200px), preview (1200px) и оригинал, с WebP для превью, и извлекать EXIF (дата, координаты, размеры).
|
||||
|
||||
#### Scenario: Генерация вариантов
|
||||
- **WHEN** фото загружено
|
||||
- **THEN** система асинхронно генерирует thumb и preview и заполняет EXIF-поля
|
||||
|
||||
### Requirement: Листинг фото с пагинацией
|
||||
Система SHALL возвращать фото альбома с cursor-пагинацией.
|
||||
|
||||
#### Scenario: Пагинация по курсору
|
||||
- **WHEN** клиент запрашивает список с курсором и лимитом
|
||||
- **THEN** система возвращает не более лимита фото и курсор следующей страницы
|
||||
|
||||
### Requirement: Получение фото по требованию
|
||||
Клиент SHALL иметь возможность получить актуальные данные фото с presigned URL.
|
||||
|
||||
#### Scenario: Выдача свежего URL
|
||||
- **WHEN** клиент запрашивает фото по id
|
||||
- **THEN** система возвращает свежий presigned URL (срок до 1 часа) для просмотра
|
||||
|
||||
### Requirement: Скачивание одиночного фото
|
||||
Любой участник, включая viewer, SHALL иметь возможность скачать одиночное фото.
|
||||
|
||||
#### Scenario: Скачивание фото
|
||||
- **WHEN** участник запрашивает скачивание фото
|
||||
- **THEN** система возвращает presigned URL оригинала
|
||||
|
||||
### Requirement: Удаление фото
|
||||
Автор SHALL иметь возможность удалить своё фото; владелец и админ SHALL иметь возможность удалить любое фото.
|
||||
|
||||
#### Scenario: Удаление своего фото
|
||||
- **WHEN** автор удаляет своё фото
|
||||
- **THEN** система удаляет запись и файлы в S3
|
||||
|
||||
#### Scenario: Удаление чужого фото участником
|
||||
- **WHEN** участник с ролью member пытается удалить чужое фото
|
||||
- **THEN** система отклоняет запрос
|
||||
@@ -0,0 +1,49 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Поток событий альбома и персональный поток через Server-Sent Events.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Подписка на события альбома
|
||||
Клиент SHALL иметь возможность подписаться на события конкретного альбома через SSE.
|
||||
|
||||
#### Scenario: Подписка на альбом
|
||||
- **WHEN** участник подключается к SSE-потоку альбома
|
||||
- **THEN** система устанавливает text/event-stream и отправляет heartbeat
|
||||
|
||||
### Requirement: События альбома
|
||||
Система SHALL публиковать события photo.created, photo.deleted, comment.created, like.added, like.removed, member.joined, member.left, album.updated.
|
||||
|
||||
#### Scenario: Новое фото
|
||||
- **WHEN** в альбоме появляется фото
|
||||
- **THEN** подписчики получают событие photo.created с метаданными и URL превью
|
||||
|
||||
### Requirement: Персональный поток
|
||||
Клиент SHALL иметь возможность подписаться на персональный поток (приглашения, уведомления).
|
||||
|
||||
#### Scenario: Подписка на персональный поток
|
||||
- **WHEN** пользователь подключается к персональному потоку
|
||||
- **THEN** система отправляет ему персональные события
|
||||
|
||||
### Requirement: Авторизация и область доступа
|
||||
Клиент SHALL подписываться только на доступные ему альбомы; токен передаётся в query-параметре.
|
||||
|
||||
#### Scenario: Подписка на недоступный альбом
|
||||
- **WHEN** пользователь подписывается на альбом, к которому нет доступа
|
||||
- **THEN** система отклоняет подключение
|
||||
|
||||
### Requirement: Устойчивость соединения
|
||||
Система SHALL отправлять heartbeat каждые 25 секунд и отключать медленных клиентов при переполнении буфера.
|
||||
|
||||
#### Scenario: Переполнение буфера клиента
|
||||
- **WHEN** буфер клиента переполняется
|
||||
- **THEN** система отключает клиента
|
||||
|
||||
### Requirement: Догон пропущенного
|
||||
Клиент SHALL иметь возможность догнать пропущенные события через REST.
|
||||
|
||||
#### Scenario: Переподключение
|
||||
- **WHEN** клиент переподключается после разрыва
|
||||
- **THEN** клиент догоняет события через запрос фото с параметром since
|
||||
@@ -0,0 +1,46 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
View-only доступ к альбому по ссылке без авторизации, с несколькими ссылками, сроком действия и отзывом.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Создание ссылки для просмотра
|
||||
Владелец и админ SHALL иметь возможность создать ссылку для просмотра альбома с заданным сроком действия.
|
||||
|
||||
#### Scenario: Создание ссылки
|
||||
- **WHEN** админ создаёт share-ссылку со сроком действия
|
||||
- **THEN** система создаёт токен и возвращает URL для просмотра
|
||||
|
||||
### Requirement: Несколько ссылок
|
||||
Альбом SHALL поддерживать несколько независимых share-ссылок.
|
||||
|
||||
#### Scenario: Отзыв одной ссылки не затрагивает другие
|
||||
- **WHEN** владелец отзывает одну ссылку
|
||||
- **THEN** остальные ссылки остаются действующими
|
||||
|
||||
### Requirement: Просмотр альбома по ссылке
|
||||
Пользователь без авторизации SHALL иметь возможность просматривать альбом по действующему токену (сетка, лайтбокс, скачивание одиночного фото).
|
||||
|
||||
#### Scenario: Просмотр по действующей ссылке
|
||||
- **WHEN** аноним открывает ссылку с действующим токеном
|
||||
- **THEN** система возвращает фото альбома со свежими presigned URL
|
||||
|
||||
#### Scenario: Истёкшая или отозванная ссылка
|
||||
- **WHEN** аноним открывает ссылку с истёкшим или отозванным токеном
|
||||
- **THEN** система отклоняет доступ
|
||||
|
||||
### Requirement: Ограничения по ссылке
|
||||
Доступ по ссылке SHALL быть только на просмотр: без комментариев, лайков, загрузки и управления участниками.
|
||||
|
||||
#### Scenario: Аноним не может комментировать
|
||||
- **WHEN** аноним по ссылке пытается добавить комментарий
|
||||
- **THEN** система отклоняет запрос
|
||||
|
||||
### Requirement: Отзыв ссылки
|
||||
Владелец и админ SHALL иметь возможность отозвать ссылку.
|
||||
|
||||
#### Scenario: Отзыв ссылки
|
||||
- **WHEN** владелец отзывает share-ссылку
|
||||
- **THEN** система прекращает доступ по этому токену
|
||||
@@ -0,0 +1,39 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Лайки и комментарии к фотографиям.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Лайк фото
|
||||
Пользователь с правом комментирования SHALL иметь возможность поставить и убрать лайк.
|
||||
|
||||
#### Scenario: Поставить лайк
|
||||
- **WHEN** участник ставит лайк фото
|
||||
- **THEN** система фиксирует лайк и публикует событие like.added
|
||||
|
||||
#### Scenario: Убрать лайк
|
||||
- **WHEN** участник убирает лайк
|
||||
- **THEN** система удаляет лайк и публикует событие like.removed
|
||||
|
||||
### Requirement: Комментарий к фото
|
||||
Пользователь с правом комментирования SHALL иметь возможность добавить комментарий.
|
||||
|
||||
#### Scenario: Добавить комментарий
|
||||
- **WHEN** участник добавляет комментарий
|
||||
- **THEN** система сохраняет комментарий и публикует событие comment.created
|
||||
|
||||
### Requirement: Список комментариев
|
||||
Система SHALL возвращать комментарии фото.
|
||||
|
||||
#### Scenario: Получение комментариев
|
||||
- **WHEN** клиент запрашивает комментарии фото
|
||||
- **THEN** система возвращает список комментариев с авторами
|
||||
|
||||
### Requirement: Удаление комментария
|
||||
Автор комментария (или владелец/админ) SHALL иметь возможность удалить комментарий.
|
||||
|
||||
#### Scenario: Удаление своего комментария
|
||||
- **WHEN** автор удаляет свой комментарий
|
||||
- **THEN** система удаляет комментарий
|
||||
@@ -0,0 +1,68 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Скелет проекта и подключения
|
||||
|
||||
- [x] 1.1 Создать структуру `backend/` (cmd/server, internal/*) и `frontend/` (SvelteKit 2 + Svelte 5 + adapter-static + Tailwind), Makefile, `.env.example`, `deploy/shared-album.service`; проверить наличие ожидаемых файлов и что `make frontend` собирает статику
|
||||
- [x] 1.2 Реализовать `internal/config` — чтение всех env (PG_*, S3_*, JWT_*, HTTP_*, SSE_*); проверить парсинг запуском бинарника с `.env.example`
|
||||
- [x] 1.3 Подключение к `pg.localnet` через pgx с ретраями; проверить `GET /api/health` возвращает 200 при доступной БД
|
||||
- [x] 1.4 Клиент S3 к Garage (AWS SDK v2, `region=garage`, `UsePathStyle=true`, BaseEndpoint) и создание бакета при отсутствии; проверить health-check пингует S3
|
||||
|
||||
## 2. Миграции и репозитории
|
||||
|
||||
- [x] 2.1 Добавить golang-migrate и миграции для таблиц: users, albums, album_members, invites, photos, comments, likes, notifications, share_links, refresh_tokens; проверить `make migrate` применяет схему к pg.localnet
|
||||
- [x] 2.2 Реализовать базовые репозитории на pgx (users, albums); проверить юнит-тесты CRUD на тестовой БД
|
||||
- [x] 2.3 Репозитории album_members, invites, share_links, refresh_tokens; проверить тесты на инвайт-токены и роли
|
||||
|
||||
## 3. Аутентификация
|
||||
|
||||
- [x] 3.1 `POST /api/auth/register` и `/login` с bcrypt и выдачей access (15 мин) + refresh (30 дней, хэш в refresh_tokens); проверить curl-сценарии регистрации/входа
|
||||
- [x] 3.2 `POST /api/auth/refresh` с ротацией refresh-токена и `POST /api/auth/logout` с отзывом; проверить повторный refresh отозванным токеном возвращает 401
|
||||
- [x] 3.3 JWT-middleware для защиты эндпоинтов, кроме `/api/auth/*` и share; проверить доступ без/с токеном
|
||||
|
||||
## 4. Альбомы и участники
|
||||
|
||||
- [x] 4.1 CRUD альбомов (`GET/POST /api/albums`, `GET/PATCH/DELETE /api/albums/:id`) с владельцем из owner_id; проверить сценарии создания и удаления альбома
|
||||
- [x] 4.2 Инвайты (`POST /api/albums/:id/invites`, `POST /api/invites/:token/accept`) с ролями и лимитом использований; проверить принятие и истечение инвайта
|
||||
- [x] 4.3 Роли и права на уровне сервиса (матрица прав, правило «нет эскалации», `PATCH/DELETE /api/albums/:id/members/:user_id`); проверить отклонение эскалации прав тестами
|
||||
|
||||
## 5. Загрузка и просмотр фото
|
||||
|
||||
- [x] 5.1 Загрузка фото в Garage (single + batch) со стримингом и генерацией thumb/preview/original; проверить загрузку одного и пачки файлов
|
||||
- [x] 5.2 Конвейер изображений: imaging (resize) + go-webp (WebP-энкод) + goexif (EXIF); проверить создание thumb 200px и preview 1200px с EXIF-полями
|
||||
- [x] 5.3 Листинг `GET /api/albums/:id/photos?cursor=&limit=` и `GET /api/photos/:id` с presigned URL (1 час); проверить cursor-пагинацию и выдачу свежих URL
|
||||
- [x] 5.4 Скачивание `GET /api/photos/:id/download` и удаление `DELETE /api/photos/:id`; проверить права на удаление (своё/чужое)
|
||||
|
||||
## 6. Лайки и комментарии
|
||||
|
||||
- [x] 6.1 Лайки (`POST/DELETE /api/photos/:id/likes`); проверить поставить/убрать лайк
|
||||
- [x] 6.2 Комментарии (`GET/POST /api/photos/:id/comments`, `DELETE /api/comments/:id`); проверить добавление, список и удаление комментария
|
||||
|
||||
## 7. SSE real-time
|
||||
|
||||
- [x] 7.1 SSE-хаб на каналах (map по album_id, неблокирующая публикация, backpressure) и `GET /api/sse/albums/:id`; проверить подключение и heartbeat 25 сек
|
||||
- [x] 7.2 Публикация событий photo.created/photo.deleted/comment.created/like.added/like.removed/member.joined/member.left/album.updated из сервисов; проверить получение события при новом фото
|
||||
- [x] 7.3 Авторизация SSE через `?token=` и проверка доступа к альбому; проверить отклонение подписки на чужой альбом
|
||||
|
||||
## 8. Уведомления
|
||||
|
||||
- [x] 8.1 Создание уведомлений при комментариях/лайках/добавлении в альбом; проверить запись в таблицу notifications
|
||||
- [x] 8.2 `GET /api/sse/me` (персональный поток) и отметка уведомлений прочитанными; проверить получение персонального события и смену статуса read_at
|
||||
|
||||
## 9. Ссылки для просмотра
|
||||
|
||||
- [x] 9.1 Создание/отзыв share-ссылок (`POST/DELETE /api/albums/:id/share`) с expires_at и несколькими ссылками; проверить создание и точечный отзыв
|
||||
- [x] 9.2 Публичные эндпоинты `/api/share/:token` (просмотр, листинг, скачивание одиночного) без авторизации; проверить доступ по действующему и отклонение по истёкшему/отозванному токену
|
||||
|
||||
## 10. Frontend SvelteKit
|
||||
|
||||
- [x] 10.1 API-обёртка (fetch) и auth-store с автообновлением при 401; проверить логин/регистрацию в браузере
|
||||
- [x] 10.2 Экраны: список альбомов, страница альбома, участники, настройки, принятие инвайта; проверить навигацию по альбомам
|
||||
- [x] 10.3 Uploader (drag&drop, прогресс), PhotoGrid (ленивая загрузка), Lightbox (свайпы); проверить загрузку и просмотр фото
|
||||
- [x] 10.4 SSE-клиент (EventSource + reconnect/backoff + догон по REST) и live-обновление сетки/комментариев; проверить live-появление фото
|
||||
- [x] 10.5 Три темы (light/dark/neutral) + system через data-theme, store на runes, ThemeSwitcher; проверить переключение без вспышки
|
||||
|
||||
## 11. Сборка и деплой
|
||||
|
||||
- [x] 11.1 Сборка frontend в статику, копирование в `backend/static`, `go:embed`, сборка одного бинарника (`CGO_ENABLED=0`); проверить `make build` даёт один артефакт
|
||||
- [x] 11.2 systemd-юнит с EnvironmentFile и харденингом, пример nginx для SSE (`proxy_buffering off`); проверить запуск бинарника и подключение к нему
|
||||
- [x] 11.3 README с инструкцией сборки, деплоя и запуска через systemd; проверить команды из README выполняются
|
||||
@@ -0,0 +1,32 @@
|
||||
schema: spec-driven
|
||||
|
||||
# Project context (optional)
|
||||
# Add only constraints that should guide OpenSpec artifacts and workflows.
|
||||
# Include constraints an agent cannot infer by reading the code.
|
||||
# Keep general project documentation and discoverable codebase facts out.
|
||||
# Example:
|
||||
# context: |
|
||||
# Designs and tasks must cover Windows, macOS, and Linux
|
||||
# Write all artifacts in Spanish
|
||||
|
||||
# Per-artifact rules (optional)
|
||||
# Add custom rules for specific artifacts.
|
||||
# Example:
|
||||
# rules:
|
||||
# proposal:
|
||||
# - Keep proposals under 500 words
|
||||
# - Always state what is out of scope
|
||||
# tasks:
|
||||
# - Break tasks into chunks of max 2 hours
|
||||
|
||||
# Per-operation guidance (optional)
|
||||
# Add advisory guidance for how apply and archive work should be conducted.
|
||||
# This is separate from artifact rules above.
|
||||
# Example:
|
||||
# operations:
|
||||
# apply:
|
||||
# guidance:
|
||||
# - Keep test summaries concise
|
||||
# archive:
|
||||
# guidance:
|
||||
# - Summarize the archive outcome before finishing
|
||||
Reference in New Issue
Block a user