feat(profile): add round avatar with upload and cropping

- backend: user_avatar_objects binding (PG+SQLite), GC preserves avatar blobs,
  avatar service (EXIF-stripping WebP 128/512), public avatar read route
- web: avatar upload/crop on profile, round avatar in header/members/comments/author
- android: avatar upload/crop, round avatar on profile and albums header
- openspec: add-user-avatar proposal/design/specs/tasks
This commit is contained in:
iu
2026-10-12 16:16:47 +03:00
parent 657a04309e
commit 04617d1cdb
49 changed files with 2362 additions and 21 deletions
+15 -3
View File
@@ -341,6 +341,7 @@ colors: {
taken_at, latitude, longitude, caption, exif, deleted_at, created_at)
- **objects** (id, storage_key, mime_type, size, width, height, checksum, state, created_at)
- **photo_objects** (photo_id, object_id, role)
- **user_avatar_objects** (user_id, object_id, role)
- **variant_jobs** (photo_id, attempts, next_attempt_at, last_error)
- **comments** (id, photo_id, user_id, text, created_at, updated_at)
- **likes** (photo_id, user_id, created_at)
@@ -372,15 +373,17 @@ colors: {
**Postgres — журнал истины.** Таблица `objects` описывает каждый блоб (ключ, mime,
размер, габариты, checksum, состояние `pending|ready|deleting`), таблица
`photo_objects` связывает фото с объектами по ролям (`original|thumb|preview`),
таблица `variant_jobs` — очередь генерации вариантов. Альбом, принадлежность и
представление живут только в `photos`/`albums`.
таблица `user_avatar_objects` — аватар пользователя с его нарезками
(`avatar|avatar_small`), таблица `variant_jobs` — очередь генерации вариантов.
Альбом, принадлежность и представление живут только в `photos`/`albums`.
Следствия:
- **Копирование** между альбомами — только новые строки `photos`+`photo_objects`,
ссылающиеся на те же объекты; операции с хранилищем не выполняются.
- **Перенос** — только `UPDATE photos.album_id`; объекты не трогаются.
- **Удаление** фото удаляет строки; фоновый воркер удаляет блоб лишь когда на
объект не осталось ни одной ссылки (сборка мусора по ledger-состояниям).
объект не осталось ни одной ссылки (фото или аватар) — сборка мусора по
ledger-состояниям.
Доступ: **только через бэкенд** — подписанный URL со сроком 1 час на
`/api/objects/<key>`, независимо от бэкенда хранилища. Для S3 (Garage) байты
@@ -402,6 +405,15 @@ colors: {
- `POST /api/auth/refresh`
- `POST /api/auth/logout`
### Profile
- `GET|PATCH|DELETE /api/auth/me`
- `POST /api/auth/me/password`
- `POST /api/auth/me/identifiers`
- `PATCH /api/auth/me/preferences`
- `POST /api/auth/me/avatar` — загрузка готового квадрата (multipart `file`)
- `DELETE /api/auth/me/avatar` — сброс к заглушке
- `GET /api/avatars/:userID/:version?size=large` — публичная отдача аватара (без авторизации)
### Albums
- `GET /api/albums`
- `POST /api/albums`