# 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` и спеках.