117 lines
9.8 KiB
Markdown
117 lines
9.8 KiB
Markdown
# Design
|
||
|
||
## Context
|
||
|
||
См. `proposal.md` — Why. Существенное для реализации:
|
||
|
||
- `users.email` сейчас `NOT NULL UNIQUE` в обеих схемах; уникальность — `users_email_key`.
|
||
- Два хранилища с общим интерфейсом `auth.Repository`: `store.go` (pgx) и `sqlite_store.go`
|
||
(`database/sql`). Две отдельные истории миграций: `backend/migrations/*.sql` (Postgres) и
|
||
`backend/migrations/sqlite/*.sql` (консолидированная + инкременты).
|
||
- `models.PublicUser` используется во всех публичных представлениях (участники, загрузчики,
|
||
комментарии). `User.Public()` конвертирует в него; `/auth/me` сейчас тоже отдаёт `Public()`.
|
||
- FK уже каскадные (`albums.owner_id`, `photos`, `comments`, `likes`, `tags`, `notifications`,
|
||
`share_links`, `invites` → `users` ON DELETE CASCADE).
|
||
- Блобы не в БД: удаление строк оставляет объекты, которые добирает `cmd/reconcile`.
|
||
- `scripts/seed.py` регистрирует по email.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- Единый идентификатор входа (email или E.164-телефон) без отправки SMS.
|
||
- Управление профилем (имя, пароль) и удаление аккаунта со всем содержимым — на backend,
|
||
web и Android.
|
||
- Миграция без потери существующих email-only аккаунтов.
|
||
|
||
**Non-Goals:**
|
||
- Верификация телефона, смена/удаление заданных логинов, avatar-загрузка, «корзина» аккаунта.
|
||
- Мгновенная очистка блобов в транзакции удаления (остаётся на `reconcile`).
|
||
|
||
## Decisions
|
||
|
||
### 1. Схема: оба идентификатора nullable, «хотя бы один» на уровне приложения
|
||
`email` и `phone` оба `NULL`-допустимы и уникальны; обязательность «хотя бы одного»
|
||
проверяет сервис регистрации/добавления, а не `CHECK` в БД. Так существующие строки и
|
||
phone-only новые аккаунты сосуществуют одинаково в обеих СУБД, а сообщения об ошибке
|
||
остаются контролируемыми. Альтернатива с `CHECK (email IS NOT NULL OR phone IS NOT NULL)`
|
||
отвергнута: в SQLite её добавление всё равно требует rebuild, а выигрыш невелик.
|
||
|
||
### 2. E.164-нормализация в сервисном слое
|
||
Отдельная функция нормализации: обязательный ведущий `+`, только цифры, длина 8–15.
|
||
Одна точка нормализации для регистрации, добавления и входа — чтобы вход был точным
|
||
сравнением по каноническому значению. Определение типа идентификатора: наличие `@` → email,
|
||
иначе → телефон.
|
||
|
||
### 3. Добавление идентификатора — отдельный add-only эндпоинт
|
||
`POST /auth/me/identifiers` с необязательными `email` и `phone`. Сервис разрешает ровно один
|
||
новый идентификатор, отклоняет попытку задать уже существующий/второй, проверяет
|
||
уникальность и конфликты (`email_taken`, `phone_taken`). Альтернатива «`PATCH /auth/me` с
|
||
опциональными полями» отвергнута: неявное смешение изменяемого (имя) и неизменяемого
|
||
(логины) в одном ресурсе.
|
||
|
||
### 4. Смена пароля: текущий пароль + отзыв всех refresh-токенов
|
||
`POST /auth/me/password { current_password, new_password }`. При успехе `RevokeAllUserTokens`.
|
||
Клиенты очищают токены и уходят на экран входа. Access-токен (≤15 мин) формально жив, но
|
||
refresh мёртв; для консистентности клиенты всё равно разлогиниваются.
|
||
|
||
### 5. Удаление аккаунта: подтверждение паролем + удаление строки
|
||
`DELETE /auth/me { password }`. Проверяем пароль, затем `DELETE FROM users WHERE id`.
|
||
Каскад FK удаляет альбомы (вместе с доступом других участников), фото, соцданные,
|
||
уведомления, share-ссылки, приглашения и токены. После удаления любой запрос с прежним
|
||
access-токеном завершается 401, потому что строки пользователя больше нет. Возврат 204.
|
||
Блобы освобождаются `reconcile`. Альтернативы (запрет при общих альбомах, передача
|
||
владения, grace-период) отклонены по решению: удалять всё и сразу.
|
||
|
||
### 6. Приватность телефона
|
||
`phone` добавляется в `models.User`, но НЕ в `PublicUser`, поэтому не попадает в списки
|
||
участников/загрузчиков/комментариев. Собственный профиль отдаётся отдельным ответом с
|
||
телефоном, а не через `Public()`.
|
||
|
||
### 7. Миграции
|
||
- Postgres `000013_user_phone`: `ALTER TABLE users ALTER COLUMN email DROP NOT NULL`,
|
||
`ADD COLUMN phone text`, `ADD CONSTRAINT users_phone_key UNIQUE (phone)`.
|
||
- SQLite `000005_user_phone`: rebuild `users` по 12-шаговому паттерну (создать новую таблицу
|
||
без `NOT NULL` у email, с `phone`, перенести данные, пересоздать индексы), т.к. SQLite
|
||
не умеет снимать ограничения через `ALTER`.
|
||
- Существующие строки получают `phone = NULL`; email сохраняется.
|
||
|
||
### 8. Клиенты
|
||
- Web: маршрут `/profile` под `(app)`, вход из шапки по имени; login-страница — одно поле
|
||
«Email или телефон»; при регистрации поля email/phone с проверкой «хотя бы одно».
|
||
- Android: `ProfileScreen` в `GaloNavHost`, вход из `AlbumsScreen`; `AuthScreen` — одно поле
|
||
идентификатора и необязательные email/phone при регистрации; по успеху пароль/удаление →
|
||
очистка `TokenStore` → на экран auth.
|
||
- Оба клиента обновляются вместе с сервером: контракт login/register — **BREAKING**.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- [Телефон-логин без верификации → занятие чужого номера] → принято осознанно для
|
||
self-hosted; задел под OTP вне объёма.
|
||
- [SQLite rebuild `users`] → выполнять в одной миграции с временным отключением FK и
|
||
пересозданием индексов; проверить на копии `galo.db`.
|
||
- [Запросы сканируют `email` в `string`; теперь возможен NULL] → перейти на
|
||
`COALESCE(email,'')` в обоих хранилищах и покрыть тестами.
|
||
- [Гонка уникальности] → опираться на unique-индексы, маппить нарушение в `email_taken`/
|
||
`phone_taken`.
|
||
- [Удаление каскадом уничтожает общие альбомы] → необратимо и покрыто подтверждением
|
||
паролем; поведение задокументировано в спеке.
|
||
- [Access-токен живёт ≤15 мин после смены пароля/удаления] → refresh отозван, при удалении
|
||
пользователь отсутствует; клиенты разлогиниваются немедленно.
|
||
- [Значение email в JWT-claim может быть пустым] → claim не обязателен для авторизации
|
||
(используется `uid`); оставляем как есть.
|
||
|
||
## Migration Plan
|
||
|
||
1. Применить миграции: Postgres `000013`, SQLite `000005` (на копии проверить rebuild).
|
||
2. Задеплоить backend с новым контрактом.
|
||
3. Обновить web и Android синхронно (единый релиз) из-за breaking login/register.
|
||
4. Проверки: регистрация email-only, phone-only, обоими; вход обоими; добавление второго
|
||
идентификатора; смена пароля (все сессии отозваны); удаление аккаунта с общими альбомами.
|
||
5. Откат: down-миграции возвращают `email NOT NULL` и удаляют `phone`; выполним только если
|
||
нет phone-only аккаунтов. Удаление аккаунтов необратимо.
|
||
|
||
## Open Questions
|
||
|
||
Нет — все развилки (верификация, обязательность, иммутабельность, политика пароля,
|
||
поведение общих альбомов, подтверждение удаления) зафиксированы в `proposal.md` и спеках.
|