# Аутентификация, роли, первый запуск ## Роли | Роль | Назначение | |---|---| | `admin` | Владелец системы. Может быть несколько, но минимум один всегда (последнего админа нельзя понизить / удалить / заблокировать). | | `moderator` | Заместитель администратора: управление обычными пользователями. | | `user` | Слушатель. | ## Матрица прав | Действие | user | moderator | admin | |---|:-:|:-:|:-:| | Слушать, поиск, коллекция, волна | ✅ | ✅ | ✅ | | Сменить свой пароль, язык, сессии | ✅ | ✅ | ✅ | | Открыть админку | — | ✅ | ✅ | | Видеть список пользователей | — | ✅ | ✅ | | Создать пользователя с ролью `user` | — | ✅ | ✅ | | Сбросить пароль / заблокировать `user` | — | ✅ | ✅ | | Создать `moderator` / `admin`, менять роли | — | — | ✅ | | Сбросить пароль / заблокировать `moderator` | — | — | ✅ | | Удалить пользователя | — | — | ✅ | | Хранилища, сканирование, переиндексация | — | — | ✅ | | Статус библиотеки (только просмотр) | — | ✅ | ✅ | | Предлагать треки (загрузка в очередь), видеть свои предложения | — | ✅ | ✅ | | Рассматривать очередь, одобрять / отклонять, править метаданные | — | — | ✅ | | Лимит очереди, целевое хранилище, шаблон раскладки | — | — | ✅ | | Статус рекомендательной системы, аудит | — | — | ✅ | Реализация: в Go — политики в `internal/auth/policy.go` вида `CanManageUser(actor, target, action) bool`, проверяются в сервисном слое (не только в middleware), покрываются табличными тестами по этой матрице. Все админские действия пишутся в `audit_log`. ## Первый запуск (bootstrap) 1. При старте `api` проверяет `app_settings['bootstrap.completed']`. Если нет и в `users` нет ни одного админа — создаёт пользователя: - `username = BOOTSTRAP_ADMIN_USERNAME` (по умолчанию `admin`); - пароль = `BOOTSTRAP_ADMIN_PASSWORD` из env, а если не задан — генерируется случайный (20 символов) и **печатается в лог один раз**; - `must_change_credentials = true`. 2. `GET /server-info` возвращает `bootstrap_required: true` — клиент может показать подсказку на экране входа. 3. После входа с `must_change_credentials = true` любые эндпоинты, кроме `/me`, `/me/bootstrap-credentials`, `/auth/logout`, отвечают `403 { code: "auth.credentials_change_required" }`. Клиент показывает экран «Смените логин и пароль администратора». 4. `POST /me/bootstrap-credentials` — новый логин (может остаться `admin`, но пароль обязан отличаться), новый пароль (≥ 12 символов для админа) → флаг снимается, `bootstrap.completed = true`, все прочие токены отзываются, выдаётся новая пара. Аналогично для `must_change_password` (после сброса пароля админом / модератором): разрешён только `POST /me/password` (без `current_password`, если вход был по временному паролю). ## Пароли - argon2id: `m=64 MiB, t=3, p=2`, соль 16 байт; параметры в хэше (PHC), при изменении — rehash при входе. - Политика: ≥ 8 символов для `user`, ≥ 12 для `moderator`/`admin`; не совпадает с логином; проверка по встроенному списку частых паролей (`internal/auth/common_passwords.txt`; пока ~150 записей, цель — 10k). - Временный пароль при создании пользователя и при сбросе генерирует сервер: 12 символов, показывается **один раз**; у пользователя ставится `must_change_password = true`. - Защита от перебора: после 5 неудачных попыток — `locked_until = now() + 5 мин` (растёт экспоненциально до 1 ч), плюс rate limit по IP в Redis. Ответ всегда одинаковый (`auth.invalid_credentials`) — не раскрываем, существует ли логин. Время ответа выравнивается (хэш с фиктивным паролем для несуществующих). ## Токены | | Access | Refresh | |---|---|---| | Формат | JWT, EdDSA (Ed25519) | 32 случайных байта, base64url; в БД — SHA-256 | | Срок | 15 мин | 30 дней скользящий (каждая ротация продлевает), абсолютный максимум 180 дней | | Claims | `sub`, `role`, `sid` (family_id), `ver` (token_version пользователя), `exp`, `iat` | — | | Веб | в памяти (JS) | cookie `pipu_rt`: `HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth` | | Android | в памяти | `expo-secure-store` | - **Ротация**: каждый `/auth/refresh` помечает старый токен `used_at` и выдаёт новый в той же `family_id`. Повторное предъявление уже использованного токена ⇒ компрометация ⇒ отзыв всей семьи (`401 auth.refresh_reused`). Исключение — первые 10 с после ротации: так бывает, когда две вкладки обновляют токен одновременно; ответ `401 auth.refresh_race` без отзыва, клиент повторяет запрос с уже новой cookie. - **Мгновенный отзыв** (блокировка, смена роли, смена пароля): у пользователя инкрементируется `token_version` (в Redis-кэше), middleware сравнивает с `ver` в access-токене. - Ключи подписи: `JWT_PRIVATE_KEY` (base64 seed Ed25519) из env; `kid` = первые 8 байт SHA-256 публичного ключа, прежние ключи для проверки — `JWT_PREVIOUS_KEYS`. - Смена пароля и учётных данных отзывает все сессии и сразу выдаёт новую пару текущему устройству — пользователь остаётся в системе, остальные устройства разлогиниваются. - CSRF: refresh-cookie `SameSite=Strict` и путь только `/api/v1/auth`; остальные запросы — Bearer-заголовок, поэтому CSRF к ним неприменим. ## Клиентская логика - Один общий модуль в `packages/core/auth`: хранит access в памяти, при `401 auth.token_expired` делает **единственный** параллельный refresh (single-flight) и повторяет запросы. - Роутинг-гарды: `must_change_credentials` → `/setup/admin`; `must_change_password` → `/setup/password`; админские разделы — по `role`.