1072 lines
67 KiB
Markdown
1072 lines
67 KiB
Markdown
# Функциональная спецификация Galaxy
|
||
|
||
Документ описывает, что делает платформа Galaxy в терминах пользовательских
|
||
операций и логики каждого сервиса, которая их реализует. Каждый раздел
|
||
проводит читателя по одному доменному сценарию: кто инициирует операцию,
|
||
что `gateway` проверяет и форвардит, что `backend` валидирует и сохраняет,
|
||
что возвращается клиенту, и какие побочные эффекты при этом запускаются
|
||
(почта, push, операции с контейнерами).
|
||
|
||
Это отправная точка для любого изменения, затрагивающего поведение системы.
|
||
Точные форматы протоколов, словарь кодов ошибок, переменные окружения,
|
||
значения по умолчанию, лимиты троттлинга, имена таблиц и колонок,
|
||
field-level-валидация — всё это лежит в нижнеуровневых источниках:
|
||
|
||
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — глобальная архитектура,
|
||
модель безопасности, транспортный контракт.
|
||
- `galaxy/<service>/README.md` — структура сервиса, конфигурация,
|
||
эксплуатация.
|
||
- `galaxy/<service>/openapi.yaml`, `*.proto` — wire-контракты.
|
||
- `galaxy/<service>/docs/flows.md` — sequence-диаграммы.
|
||
|
||
Этот файл сознательно опускает такие детали. Если этот файл расходится
|
||
с нижнеуровневым источником, см. правило синхронизации в проектном
|
||
`CLAUDE.md`.
|
||
|
||
> **Внимание.** Этот файл — перевод английского
|
||
> [`FUNCTIONAL.md`](FUNCTIONAL.md) и **не является источником истины**.
|
||
> Авторитетна английская версия; при расхождении выигрывает она.
|
||
> Каждое точечное изменение в `FUNCTIONAL.md` должно быть зеркально
|
||
> внесено сюда в том же патче (переводить только затронутые абзацы).
|
||
> Полный перевод заново выполняется только по явному запросу владельца
|
||
> проекта.
|
||
|
||
Документ организован по доменным сценариям, не по группам HTTP-маршрутов.
|
||
Публичные, user-аутентифицированные и admin-операции могут оказаться в
|
||
одном разделе, если все они участвуют в одном бизнес-флоу.
|
||
|
||
## Содержание
|
||
|
||
1. [Аутентификация и устройство-сессия](#1-аутентификация-и-устройство-сессия)
|
||
2. [Управление аккаунтом](#2-управление-аккаунтом)
|
||
3. [Жизненный цикл игры в лобби](#3-жизненный-цикл-игры-в-лобби)
|
||
4. [Участие в лобби](#4-участие-в-лобби)
|
||
5. [Реестр названий рас](#5-реестр-названий-рас)
|
||
6. [Игровая сессия](#6-игровая-сессия)
|
||
7. [Канал push](#7-канал-push)
|
||
8. [Уведомления и почта](#8-уведомления-и-почта)
|
||
9. [Гео-сигнал](#9-гео-сигнал)
|
||
10. [Администрирование](#10-администрирование)
|
||
|
||
---
|
||
|
||
## 1. Аутентификация и устройство-сессия
|
||
|
||
Раздел описывает, как анонимный клиент становится аутентифицированным
|
||
и остаётся таковым, пока серверное действие не отзовёт эти полномочия.
|
||
|
||
### 1.1 Состав
|
||
|
||
В составе: выпуск e-mail-вызова на вход, его подтверждение (с созданием
|
||
аккаунта при первом входе и регистрацией публичного ключа клиента),
|
||
создание устройства-сессии, поиск сессии для каждого аутентифицированного
|
||
запроса, отзыв сессии со стороны сервера.
|
||
|
||
Вне состава: формат конверта и схема подписи, которые используются
|
||
каждым аутентифицированным запросом — определены однажды в
|
||
[ARCHITECTURE.md §15](ARCHITECTURE.md#15-transport-security-model-gateway-boundary)
|
||
и переиспользуются всеми последующими разделами; хранение ключа
|
||
на стороне клиента; маршрутизация push-событий внутри gateway к
|
||
конкретному стриму подписчика.
|
||
|
||
### 1.2 Выпуск вызова на вход
|
||
|
||
Клиент отправляет e-mail на публичный auth-маршрут gateway.
|
||
Маршрут не аутентифицирован — ещё нет устройства-сессии, к которому
|
||
можно было бы привязаться.
|
||
|
||
Gateway относится к этому маршруту как к более строгому классу
|
||
"public auth": применяет per-IP и per-identity (per-email) anti-abuse,
|
||
ограничение на размер тела, allow-list HTTP-методов, после чего
|
||
форвардит запрос в backend. Сбои upstream-адаптера проецируются обратно
|
||
клиенту с тем же статусом и envelope-ошибкой; транспортные сбои —
|
||
обобщённым ответом "недоступно".
|
||
|
||
Backend выпускает непрозрачный идентификатор вызова и отправляет
|
||
письмо подтверждения через durable mail outbox. **Форма ответа
|
||
идентична независимо от того, принадлежит ли e-mail существующему
|
||
аккаунту, новому или попадает под троттлинг** — endpoint нельзя
|
||
использовать для перечисления аккаунтов.
|
||
|
||
Ветки внутри backend:
|
||
|
||
- **Permanent block.** Если адрес заблокирован на уровне аккаунта,
|
||
запрос отклоняется. Это единственная account-state-ветка, которая
|
||
отдаёт отдельный код ошибки; все прочие ветки возвращают стандартную
|
||
форму с challenge-id.
|
||
- **Throttle.** Если для одного e-mail в окне троттлинга уже
|
||
существует слишком много непогашенных и не истёкших вызовов,
|
||
backend переиспользует последний имеющийся вызов вместо создания
|
||
нового. Клиент получает ту же форму ответа и не знает о повторе.
|
||
- **Иначе.** Backend создаёт новый вызов с разрешённым preferred_language
|
||
(выводится из опционального заголовка `Accept-Language`,
|
||
форварднутого gateway, с откатом на дефолт) и в той же транзакции
|
||
ставит auth-mail-строку прямо в outbox. SMTP-доставка асинхронна;
|
||
auth-ответ возвращается, как только строки challenge и outbox
|
||
durably закоммитены.
|
||
|
||
### 1.3 Подтверждение вызова
|
||
|
||
Клиент отправляет challenge id, код из письма, свежий публичный ключ
|
||
Ed25519 и выбранную IANA-таймзону. Gateway применяет тот же
|
||
public-auth anti-abuse-класс, но per-identity-бакет ключуется по
|
||
challenge id, а не по e-mail. `Accept-Language` на этом endpoint
|
||
не учитывается — preferred_language был зафиксирован на этапе send
|
||
и проигрывается из строки challenge.
|
||
|
||
Backend валидирует challenge под row lock: отклоняет неизвестные,
|
||
истёкшие или уже погашенные id, инкрементирует счётчик попыток и
|
||
сжигает challenge при достижении потолка. После того как код
|
||
сошёлся, backend перепроверяет permanent-block-флаг — это ловит
|
||
случай, когда админ применил блок между send и confirm — и отклоняет
|
||
запрос, если флаг выставлен. На успешном пути backend гарантирует
|
||
существование аккаунта (синтезирует неизменяемый display-handle
|
||
только при первом входе и заполняет declared_country по source IP),
|
||
после чего помечает challenge consumed и в той же транзакции создаёт
|
||
устройство-сессию, привязанную к публичному ключу вызывающего.
|
||
Ответ несёт идентификатор новой устройства-сессии.
|
||
|
||
Challenge — single-use. Повторное подтверждение того же id возвращает
|
||
ту же непрозрачную форму `invalid_request`, что и подтверждение
|
||
неизвестного или истёкшего id; API сознательно не различает эти
|
||
три случая, чтобы атакующий не мог майнить состояние challenge.
|
||
Throttle-переиспользование на стороне send означает, что клиент,
|
||
попавший под троттлинг, получит обратно последний существующий
|
||
`challenge_id` вместо свежего, но каждый id всё равно гасится ровно
|
||
один раз.
|
||
|
||
### 1.4 Поиск сессии для каждого запроса
|
||
|
||
Когда у клиента есть идентификатор устройства-сессии и приватный ключ,
|
||
каждый аутентифицированный вызов — это подписанный gRPC-запрос к
|
||
gateway. Gateway — единственный компонент, который видит подпись
|
||
запроса; backend доверяет вердикту gateway.
|
||
|
||
Gateway нужен публичный ключ сессии для проверки подписи, поэтому
|
||
каждый аутентифицированный запрос разрешает устройство-сессию через
|
||
in-memory LRU-кэш (с ограничением на число записей плюс TTL-страховка).
|
||
При промахе кэш зовёт endpoint backend для поиска сессии и заполняет
|
||
запись. Gateway отклоняет запрос, если кэш сообщает "сессия неизвестна"
|
||
или "отозвана"; иначе он проверяет конверт согласно
|
||
[ARCHITECTURE.md §15](ARCHITECTURE.md#15-transport-security-model-gateway-boundary)
|
||
и форвардит проверенный payload в backend по обычному REST,
|
||
инжектируя в заголовке резолвлёный user_id. Backend никогда не
|
||
выводит identity из тела запроса.
|
||
|
||
Backend обновляет `last_seen_at` в строке сессии при каждом успешном
|
||
поиске — это даёт админам видимость того, когда каждая закэшированная
|
||
сессия в последний раз резолвилась на edge. Обновление — часть
|
||
транзакции поиска; сбои логируются, но не пропагируются вызывающему.
|
||
|
||
Кэш инвалидируется через push-канал, а не через периодический
|
||
рефреш: событие `session_invalidation` переключает статус закэшированной
|
||
записи на revoked, после чего последующие запросы, привязанные к
|
||
этой сессии, отклоняются без повторного похода в backend. TTL — это
|
||
страховка на случай потерянных событий (курсор устарел, gateway
|
||
перезапустился) — в установившемся режиме push-события являются
|
||
авторитетным источником инвалидации.
|
||
|
||
### 1.5 Отзыв
|
||
|
||
Отзыв делает устройство-сессию неспособной аутентифицировать любой
|
||
будущий запрос и принуждает закрыться все push-стримы, привязанные
|
||
к ней. Триггеры разделяются на две группы.
|
||
|
||
**Инициированный пользователем (logout).** User-surface предоставляет
|
||
три операции: получить свои активные сессии, отозвать одну и отозвать
|
||
все. Gateway форвардит их в backend как обычные аутентифицированные
|
||
запросы. Backend проверяет, что целевая сессия принадлежит
|
||
вызывающему (иначе возвращает ту же форму, что и отсутствующая
|
||
сессия — чужие session id не могут быть зондированы), атомарно
|
||
переключает `device_sessions.status` на `revoked` и вставляет строку
|
||
в `session_revocations`, после чего публикует одно
|
||
`session_invalidation`-событие на каждую отозванную сессию.
|
||
|
||
**Инициированный админом и lifecycle.** Санкции, подразумевающие
|
||
отзыв сессий (сейчас — `permanent_block`), admin-инициированный
|
||
soft-delete и пользовательский self-soft-delete — все они приводят
|
||
к in-process-вызову внутри backend. Действуют те же атомарные
|
||
UPDATE + audit-insert + push-эмиссия; audit-строка несёт другой
|
||
`actor_kind`
|
||
(`admin_sanction` / `soft_delete_admin` / `soft_delete_user`).
|
||
|
||
Когда backend опубликовал push-событие, gateway переключает
|
||
закэшированную запись сессии на revoked и закрывает все активные
|
||
push-стримы, привязанные к ней. Per-request internal-поиск против
|
||
backend остаётся durable-страховкой: если push-событие потеряно,
|
||
следующий поиск (после истечения TTL кэша) вернёт уже отозванную
|
||
запись.
|
||
|
||
`session_revocations` — это аудит-журнал. Каждая строка несёт
|
||
`revocation_id`, `device_session_id`, `user_id`, `actor_kind`, пару
|
||
полей актора (`actor_user_id` для user-driven kind'ов,
|
||
`actor_username` для admin-driven kind'ов — ровно одно из двух
|
||
заполнено в каждой строке), `reason` и `revoked_at`. Операторы могут
|
||
запрашивать её, чтобы ответить "кто и почему отозвал эту сессию";
|
||
таблица append-only.
|
||
|
||
Endpoint `/api/v1/internal/sessions/{id}` в backend — read-only:
|
||
он несёт per-request session lookup, который gateway использует
|
||
для проверки подписанных конвертов. Internal revoke-endpoints
|
||
больше не существуют; revoke инициируется либо пользователем
|
||
(через user-surface), либо админом (через in-process-вызов внутри
|
||
backend).
|
||
|
||
### 1.6 Перекрёстные ссылки
|
||
|
||
- Wire-конверт, подпись, окно свежести, anti-replay:
|
||
[ARCHITECTURE.md §15](ARCHITECTURE.md#15-transport-security-model-gateway-boundary).
|
||
- Зоны ответственности backend-модулей `auth`, `user`, `geo`, `mail`,
|
||
`push`: [ARCHITECTURE.md §4](ARCHITECTURE.md#4-backend-domain-modules)
|
||
и `backend/README.md`.
|
||
- Семантика mail outbox для шаблона auth login-code:
|
||
[ARCHITECTURE.md §11](ARCHITECTURE.md#11-mail-outbox).
|
||
- Фрейминг push-канала и правила переподключения:
|
||
[ARCHITECTURE.md §8](ARCHITECTURE.md#8-backend--gateway-communication).
|
||
Пользовательская семантика push — в
|
||
[Разделе 7](#7-канал-push) этого документа.
|
||
|
||
---
|
||
|
||
## 2. Управление аккаунтом
|
||
|
||
Раздел описывает, что аутентифицированный пользователь может читать
|
||
или менять в своём аккаунте и как удалить аккаунт.
|
||
|
||
### 2.1 Состав
|
||
|
||
В составе: чтение агрегата аккаунта, обновление мутабельного слайса
|
||
профиля, обновление настроек (preferred_language, time_zone,
|
||
declared_country), пользовательский soft-delete.
|
||
|
||
Вне состава: admin-side-мутации того же аккаунта (санкции, лимиты,
|
||
изменения entitlement, admin-soft-delete) — описаны в
|
||
[Разделе 10](#10-администрирование). Переключение permanent_block —
|
||
только для админов.
|
||
|
||
### 2.2 Агрегат аккаунта
|
||
|
||
Backend предоставляет один read-endpoint, который возвращает агрегат
|
||
аккаунта вызывающего: durable-идентифицирующие поля (неизменяемый
|
||
display-handle, e-mail), мутабельные слайсы profile и settings, текущий
|
||
снимок entitlement, любые активные санкции и per-user-overrides
|
||
лимитов. Агрегат — авторитетный клиентский взгляд "что платформа
|
||
обо мне знает".
|
||
|
||
Display-handle синтезируется при первом входе
|
||
([Раздел 1.3](#13-подтверждение-вызова)) и никогда не перезаписывается
|
||
ни при последующих входах, ни при апдейтах профиля. Клиенты должны
|
||
относиться к нему как к стабильному идентификатору, а не как к
|
||
display-предпочтению.
|
||
|
||
### 2.3 Обновление профиля и настроек
|
||
|
||
Два различных мутирующих endpoint'а разделяют user-управляемые поля
|
||
по природе изменения. Оба следуют PATCH-семантике — отсутствующие
|
||
поля не трогаются, присутствующие заменяют сохранённое значение —
|
||
и оба возвращают обновлённый агрегат.
|
||
|
||
Profile несёт одно display-ориентированное поле: `display_name`.
|
||
Явно пустое значение очищает сохранённое имя; пропуск поля
|
||
оставляет его нетронутым.
|
||
|
||
Settings несёт locale- и timezone-предпочтения:
|
||
`preferred_language` (BCP 47-тег) и `time_zone` (IANA-идентификатор).
|
||
Оба должны быть непустыми после trim, если они присутствуют;
|
||
timezone валидируется по IANA-базе перед коммитом.
|
||
|
||
`declared_country` **не** входит ни в один из patch'ей. Backend
|
||
пишет его один раз при регистрации из source IP
|
||
([Раздел 9](#9-гео-сигнал)) и считает неизменяемым после; нет
|
||
user-видимого пути его изменить.
|
||
|
||
### 2.4 Удаление аккаунта пользователем
|
||
|
||
Пользователь может попросить backend soft-delete'нуть свой аккаунт.
|
||
Backend помечает строку аккаунта удалённой и запускает in-process-каскад,
|
||
описанный в [ARCHITECTURE.md §7](ARCHITECTURE.md#7-in-process-async-patterns).
|
||
Конкретно:
|
||
|
||
- Каждая устройство-сессия пользователя отзывается
|
||
([Раздел 1.5](#15-отзыв)) — одна audit-строка на сессию и одно
|
||
`session_invalidation`-push-событие на сессию.
|
||
- Активные membership'ы переходят в `removed` (admin-инициированный
|
||
блок переключает их в `blocked`); pending-заявки переходят
|
||
в `rejected`; входящие приглашения — в `declined`; исходящие
|
||
приглашения — в `revoked`.
|
||
- Race-name-записи, принадлежащие пользователю — registered,
|
||
reservation или pending_registration — удаляются одной cascade-
|
||
записью.
|
||
- Owned-игры в не-running-статусах (`draft`, `enrollment_open`,
|
||
`ready_to_start`, `start_failed`, `paused`) отменяются. Owned-игры
|
||
уже в `running` каскадом **не** отменяются — engine-контейнер
|
||
продолжает выпускать ходы, пока не завершится естественно;
|
||
только membership-cleanup отвязывает пользователя.
|
||
- Один `lobby.membership.removed`-веер уведомлений уходит
|
||
пользователю с `reason=removed` (или `reason=blocked` для
|
||
admin-block-пути).
|
||
|
||
Endpoint не возвращает тела. Каскад best-effort внутри одного
|
||
процесса: если downstream-модуль падает, ошибка логируется,
|
||
но аккаунт остаётся помеченным удалённым.
|
||
|
||
### 2.5 Перекрёстные ссылки
|
||
|
||
- Admin-аналоги (sanction, limit, entitlement, soft delete):
|
||
[Раздел 10](#10-администрирование).
|
||
- Контракт каскада "user blocked / user deleted":
|
||
[ARCHITECTURE.md §7](ARCHITECTURE.md#7-in-process-async-patterns).
|
||
- Виды уведомлений, эмитящихся каскадом:
|
||
[`backend/README.md` §10](../backend/README.md#10-notification-catalog).
|
||
|
||
---
|
||
|
||
## 3. Жизненный цикл игры в лобби
|
||
|
||
Раздел описывает жизнь одной игры от создания до терминального
|
||
состояния. [Раздел 4](#4-участие-в-лобби) описывает, как игроки
|
||
присоединяются к существующей игре; этот раздел сосредоточен на
|
||
самой игре.
|
||
|
||
### 3.1 Состав
|
||
|
||
В составе: создание игры (private или public), обновление её
|
||
мутабельной конфигурации, переходы по машине состояний лобби,
|
||
отмена, повтор failed-старта, терминальные переходы (`finished`,
|
||
`cancelled`).
|
||
|
||
Вне состава: заявки, приглашения, membership'ы
|
||
([Раздел 4](#4-участие-в-лобби)), Race Name Directory-промоушен
|
||
при завершении ([Раздел 5](#5-реестр-названий-рас)), engine-команды
|
||
во время running-фазы ([Раздел 6](#6-игровая-сессия)).
|
||
|
||
### 3.2 Машина состояний
|
||
|
||
Машина состояний лобби — закрытый граф, описанный в
|
||
[ARCHITECTURE.md §7](ARCHITECTURE.md#7-in-process-async-patterns):
|
||
|
||
```text
|
||
draft → enrollment_open → ready_to_start → starting → running ↔ paused → finished
|
||
↳ start_failed → ready_to_start (retry)
|
||
cancelled достижим из любого pre-finished-состояния.
|
||
```
|
||
|
||
Два базовых правила:
|
||
|
||
- **Тип владения определяет surface.** Privatе-игры несут
|
||
`owner_user_id`; переходы инициирует владелец через user-surface.
|
||
Публичные игры — collective ownership админов
|
||
(`owner_user_id IS NULL`); их переходы и изменения конфигурации
|
||
идут через admin-surface.
|
||
- **Runtime-callback владеет одним переходом.** `starting → running`
|
||
и `starting → start_failed` — единственные переходы, которые
|
||
производит runtime-модуль, после того как engine-контейнер
|
||
полностью поднялся или подтвердил сбой. Каждый прочий переход —
|
||
user- или admin-действие.
|
||
|
||
### 3.3 Создание
|
||
|
||
Пользователь создаёт private-игру через user-surface. Backend
|
||
записывает новую игру с `owner_user_id`, равным вызывающему,
|
||
visibility `private`, в состоянии `draft`, с конфигурацией из
|
||
тела запроса в качестве начальных значений.
|
||
|
||
Public-игры создаются исключительно через admin-surface
|
||
([Раздел 10](#10-администрирование)). User-surface никогда не
|
||
производит public-игру; асимметрия enforced в backend, не на
|
||
уровне маршрута.
|
||
|
||
### 3.4 Прямые переходы
|
||
|
||
Владельцы инициируют прямые переходы через специальные endpoint'ы
|
||
(`open-enrollment`, `ready-to-start`, `start`, `pause`, `resume`,
|
||
`retry-start`). Каждый endpoint:
|
||
|
||
- проверяет владение игрой (или admin-scope для public-игр);
|
||
- проверяет, что исходное состояние совпадает с предусловием
|
||
перехода, отклоняя с conflict иначе;
|
||
- обновляет lobby-запись и публикует все user-видимые уведомления,
|
||
привязанные к переходу.
|
||
|
||
`start` ставит в очередь runtime-job (длинный pull / start /
|
||
init контейнера) и сразу возвращает "queued". Финальное движение
|
||
состояния (`starting → running` или `starting → start_failed`)
|
||
приходит позже через runtime-callback. `retry-start` возвращает
|
||
`start_failed`-игру в `ready_to_start` и позволяет владельцу
|
||
снова дёрнуть `start`.
|
||
|
||
`pause` и `resume` переключают между `running` и `paused`. Запущенный
|
||
engine-контейнер не сносится при pause; меняются только lobby-расписание
|
||
и флаги приёма команд.
|
||
|
||
`ready-to-start` всегда — explicit-действие владельца (или админа),
|
||
никогда не auto-fired. Переход проверяет, что число одобренных
|
||
участников не меньше `min_players`, и иначе отклоняет с conflict.
|
||
|
||
### 3.5 Отмена и завершение
|
||
|
||
`cancel` достижим из любого pre-finished-состояния. Владельцы
|
||
могут отменять свои игры; админы — любые. Отмена примиряет
|
||
оставшиеся заявки, приглашения и membership'ы; она не повышает
|
||
race-name-резервации.
|
||
|
||
`finished` производится внутри backend, после того как engine
|
||
сообщает о завершении игры. Переход сносит engine-контейнер,
|
||
замораживает lobby-запись и триггерит Race Name Directory-промоушен
|
||
для capable-finishes ([Раздел 5](#5-реестр-названий-рас)). Оба
|
||
терминальных состояния поглощающие.
|
||
|
||
### 3.6 Админские оверрайды
|
||
|
||
Администраторы могут делать `force-start`, `force-stop` и
|
||
`ban-member` на любой игре (public или private), независимо от
|
||
состояния. `force-stop` переводит игру в stopped-состояние и
|
||
сносит engine-контейнер; `ban-member` удаляет membership и
|
||
запрещает пользователю снова присоединиться
|
||
([Раздел 4](#4-участие-в-лобби)).
|
||
|
||
### 3.7 Перекрёстные ссылки
|
||
|
||
- Словарь машины состояний и правила переходов:
|
||
[ARCHITECTURE.md §7](ARCHITECTURE.md#7-in-process-async-patterns).
|
||
- Жизненный цикл runtime-job (асинхронная работа за `start`):
|
||
[ARCHITECTURE.md §13](ARCHITECTURE.md#13-container-lifecycle-in-process)
|
||
и `backend/docs/flows.md`.
|
||
- Public-vs-private-инварианты и поддерживающий их частичный индекс:
|
||
[ARCHITECTURE.md §4](ARCHITECTURE.md#4-backend-domain-modules).
|
||
|
||
---
|
||
|
||
## 4. Участие в лобби
|
||
|
||
Раздел описывает всё, что связано с присоединением и выходом из
|
||
существующей игры: заявки (для public), приглашения (для private)
|
||
и membership'ы (после успешного присоединения).
|
||
|
||
### 4.1 Состав
|
||
|
||
В составе: подача заявки в public-игру, одобрение / отклонение
|
||
заявки владельцем или админом, выпуск и активация приглашений,
|
||
отказ получателя и отзыв выпустившим, листинг membership'ов
|
||
для игры, удаление или блокировка участника.
|
||
|
||
Вне состава: сама машина состояний игры
|
||
([Раздел 3](#3-жизненный-цикл-игры-в-лобби)) и in-game-команды,
|
||
когда участник уже играет ([Раздел 6](#6-игровая-сессия)).
|
||
|
||
### 4.2 Заявки (public-игры)
|
||
|
||
Пользователь подаёт заявку в игру по id. Заявки **принимаются
|
||
только в public-играх**; попытка против private-игры отклоняется
|
||
с conflict. Игра должна дополнительно быть в `enrollment_open`
|
||
(единственное enrolment-принимающее состояние для заявок).
|
||
Backend также отклоняет запрос, если пользователь уже member или
|
||
в block-листе игры (через `ban-member`). Иначе сохраняет заявку
|
||
как `pending` и эмитит уведомление в admin-канал.
|
||
|
||
Владелец — или администратор для public-игр — одобряет или
|
||
отклоняет заявку через специальные endpoint'ы. Одобрение создаёт
|
||
membership для подающего и эмитит соответствующее уведомление.
|
||
Отклонение просто записывает терминальное состояние; membership
|
||
не появляется.
|
||
|
||
### 4.3 Приглашения (private-игры)
|
||
|
||
Приглашения **принимаются только в private-играх**; попытка
|
||
выпустить для public-игры отклоняется с conflict. Владелец
|
||
выпускает приглашение, пока игра в `draft`, `enrollment_open`
|
||
или `ready_to_start`.
|
||
|
||
Сосуществуют две разновидности:
|
||
|
||
- **User-bound** — установлен `invited_user_id`; погасить может
|
||
только этот пользователь. Эмитится уведомление
|
||
`lobby.invite.received` получателю.
|
||
- **Code-based** — `invited_user_id` пуст; backend минтит hex-код
|
||
при выпуске, и любой вызывающий, знающий код, может погасить.
|
||
При выпуске уведомление не эмитится (получатель ещё не привязан).
|
||
|
||
Каждое приглашение несёт срок действия (по умолчанию из
|
||
конфигурации, если тело пропускает `expires_at`). Получатель
|
||
гасит (создаёт membership) или отклоняет; выпустивший может
|
||
отозвать выданное приглашение в любое время до погашения.
|
||
|
||
### 4.4 Membership'ы
|
||
|
||
Membership'ы перечисляют игроков, прикреплённых к игре. Владельцы
|
||
могут удалить или заблокировать члена; член может также удалить
|
||
сам себя. Удаление чисто прекращает участие; блок дополнительно
|
||
запрещает тому же пользователю снова подаваться или гасить
|
||
будущее приглашение для той же игры.
|
||
|
||
Admin-surface предоставляет `ban-member` как cross-game-policy-
|
||
аналог owner-блока.
|
||
|
||
### 4.5 Личные списки
|
||
|
||
User-surface предоставляет три "my"-листинга (games, applications,
|
||
invites). Они проецируют участие вызывающего по всем играм без
|
||
необходимости заранее знать game-id'ы — это даёт возможность для
|
||
dashboard- и inbox-вью.
|
||
|
||
### 4.6 Уведомления
|
||
|
||
Каждое изменение состояния в этом разделе эмитит уведомление из
|
||
каталога: `lobby.invite.received`, `lobby.invite.revoked`,
|
||
`lobby.application.submitted`, `lobby.application.approved`,
|
||
`lobby.application.rejected`, `lobby.membership.removed`,
|
||
`lobby.membership.blocked`. [Раздел 8](#8-уведомления-и-почта)
|
||
описывает веер.
|
||
|
||
### 4.7 Перекрёстные ссылки
|
||
|
||
- Жизненный цикл игры: [Раздел 3](#3-жизненный-цикл-игры-в-лобби).
|
||
- Каталог уведомлений и веер: [Раздел 8](#8-уведомления-и-почта)
|
||
и [`backend/README.md` §10](../backend/README.md#10-notification-catalog).
|
||
|
||
---
|
||
|
||
## 5. Реестр названий рас
|
||
|
||
Раздел описывает, как игрок выбирает имя своей in-game-расы и в
|
||
итоге получает это имя зарегистрированным платформенно.
|
||
|
||
### 5.1 Состав
|
||
|
||
В составе: трёхуровневый реестр (registered, reservation,
|
||
pending_registration), промоушен через "capable finish",
|
||
пользовательский промоушен pending_registration в registered,
|
||
sweeper-релиз по истечению TTL, уникальность через canonical-key-
|
||
модель.
|
||
|
||
Вне состава: как движок реально потребляет выбранное имя — это
|
||
живёт в [Разделе 6](#6-игровая-сессия).
|
||
|
||
### 5.2 Три уровня
|
||
|
||
- **Registered** — platform-unique. У одного canonical-key —
|
||
не более одного живого binding к одному пользователю.
|
||
- **Reservation** — per-game. Один и тот же canonical-key может
|
||
быть зарезервирован одним и тем же пользователем в нескольких
|
||
активных играх одновременно, но два разных пользователя не
|
||
могут зарезервировать один canonical-key в одной игре.
|
||
- **Pending registration** — переходный уровень между reservation
|
||
и registered. Выпускается автоматически после "capable finish"
|
||
(игра завершилась с тем, что игрок вырастил начальные значения
|
||
планет и популяции) и даёт пользователю окно времени, чтобы
|
||
превратить reservation в постоянную registered-запись.
|
||
|
||
### 5.3 Канонизация
|
||
|
||
Каждое имя (введённое пользователем или зарегистрированное
|
||
платформой) сворачивается в canonical key. Канонизация confusable-
|
||
aware (latin-cyrillic-look-alikes, цифро-буквенные подмены) и
|
||
применяется единообразно по реестру; уникальность enforced
|
||
по canonical-key, не по отображаемому имени. Cross-tier-конфликты
|
||
по одному и тому же canonical-key блокируются на write через
|
||
per-canonical advisory lock.
|
||
|
||
### 5.4 Путь продвижения
|
||
|
||
Reservation появляется, когда игрок именует свою расу во время
|
||
игры. Когда игра capable-завершается, backend автоматически
|
||
конвертирует reservation в pending_registration с TTL. Пока
|
||
pending-запись жива, пользователь может вызвать registration-
|
||
endpoint, чтобы продвинуть запись в `registered`. Если TTL
|
||
истёк раньше, периодический sweeper освобождает запись;
|
||
canonical-key снова доступен.
|
||
|
||
Pending registration может claim'нуть только пользователь,
|
||
который её заработал; backend отклоняет попытку другого
|
||
пользователя, даже если canonical-key совпадает.
|
||
|
||
### 5.5 Уведомления
|
||
|
||
Реестр эмитит `lobby.race_name.registered`,
|
||
`lobby.race_name.pending` и `lobby.race_name.expired`
|
||
владеющему пользователю. [Раздел 8](#8-уведомления-и-почта)
|
||
описывает веер.
|
||
|
||
### 5.6 Перекрёстные ссылки
|
||
|
||
- Библиотека канонизации и записи глоссария
|
||
("canonical key", "capable finish"):
|
||
[ARCHITECTURE.md §19](ARCHITECTURE.md#19-glossary).
|
||
- Триггер промоушена внутри lobby-модуля:
|
||
[ARCHITECTURE.md §7](ARCHITECTURE.md#7-in-process-async-patterns)
|
||
(`lobby.OnGameFinished`) и `backend/docs/flows.md`.
|
||
|
||
---
|
||
|
||
## 6. Игровая сессия
|
||
|
||
Раздел описывает, что делает активный игрок, пока идёт игра:
|
||
посылает команды и приказы, читает отчёты по ходам.
|
||
|
||
### 6.1 Состав
|
||
|
||
В составе: подача команд, подача приказов, чтение отчёта,
|
||
turn-cutoff-поведение, которое закрывает окно команд во время
|
||
генерации.
|
||
|
||
Вне состава: как сам engine-контейнер запускается, планируется
|
||
или останавливается — это runtime-вопросы, описанные в
|
||
[Разделе 3](#3-жизненный-цикл-игры-в-лобби) (start / stop) и
|
||
[Разделе 10](#10-администрирование) (admin-runtime-оверрайды).
|
||
Wire-формат команд, приказов и отчётов — собственный контракт
|
||
движка, здесь не дублируется.
|
||
|
||
### 6.2 Роль backend: pass-through с авторизацией
|
||
|
||
Signed-gRPC-конвейер для in-game-трафика использует три message
|
||
types на аутентифицированной поверхности — `user.games.command`,
|
||
`user.games.order`, `user.games.report` — у каждого типизированный
|
||
FlatBuffers-payload. Gateway транскодирует FB-запрос в JSON-форму,
|
||
которую ждёт backend, форвардит её REST'ом в соответствующий
|
||
`/api/v1/user/games/{game_id}/*` endpoint, после чего транскодирует
|
||
JSON-ответ обратно в FB перед подписью.
|
||
|
||
Для каждого in-game-endpoint user-surface работает как
|
||
авторизующий pass-through к engine-контейнеру. Backend:
|
||
|
||
- проверяет, что вызывающий — активный member целевой игры
|
||
и что игра в состоянии, принимающем операцию;
|
||
- ребиндит поле `actor` в теле на race-name вызывающего из
|
||
runtime-player-mapping (клиент не несёт доверенный actor);
|
||
- резолвит engine-endpoint (запущенный контейнер для `game_id`)
|
||
и форвардит вызов;
|
||
- возвращает payload-ответа движка клиенту без переинтерпретации.
|
||
|
||
Backend не парсит содержимое payload команд или приказов сверх
|
||
того, что требует авторизация. Движок — источник истины
|
||
о валидности и порядке in-game-решений. Gateway знает типизированную
|
||
FB-форму только чтобы транскодировать wire-формат; per-command-
|
||
семантика живёт в движке.
|
||
|
||
### 6.3 Окно хода
|
||
|
||
Запущенная игра постоянно чередуется между окном приёма команд
|
||
и фазой генерации. Переход `running → generation_in_progress` —
|
||
cutoff: любая команда или приказ, пришедшие после cutoff,
|
||
отклоняются backend до форварда, потому что движок больше не
|
||
принимает запись для закрывающегося хода. После окончания
|
||
генерации backend заново открывает окно для следующего хода.
|
||
|
||
`force-next-turn` (admin) планирует one-shot-доп-тик, который
|
||
сдвигает следующий запланированный ход на один cron-шаг.
|
||
|
||
### 6.4 Отчёты
|
||
|
||
Per-turn-отчёты — read-only-вью, забираемые из движка по запросу.
|
||
Backend авторизует вызывающего и форвардит запрос; в этом пути
|
||
нет ни кэширования, ни денормализации.
|
||
|
||
### 6.5 Побочные эффекты
|
||
|
||
Успешная генерация хода публикует runtime-snapshot в lobby-модуль,
|
||
который обновляет денормализованное вью (текущий ход, runtime-
|
||
status, per-player-stats). Engine-отчёт "game finished" гонит
|
||
переход `running → finished`
|
||
([Раздел 3.5](#35-отмена-и-завершение)) и триггерит Race Name
|
||
Directory-промоушен ([Раздел 5](#5-реестр-названий-рас)).
|
||
|
||
`game.*`-виды уведомлений (`game.started`, `game.turn.ready`,
|
||
`game.generation.failed`, `game.finished`) зарезервированы в
|
||
документации, но **не имеют поставщика** в кодовой базе сегодня;
|
||
notification-каталог явно их опускает
|
||
(`backend/internal/notification/catalog.go`). Добавление поставщика
|
||
аддитивно: зарегистрировать вид в каталоге, заполнить
|
||
`MailTemplateID`, если нужен email-веер, и заставить нужный
|
||
доменный модуль вызвать `notification.Submit`.
|
||
|
||
### 6.6 Перекрёстные ссылки
|
||
|
||
- Backend ↔ engine wire-контракт (`pkg/model/{order,report,rest}`):
|
||
[ARCHITECTURE.md §9](ARCHITECTURE.md#9-backend--game-engine-communication).
|
||
- Жизненный цикл контейнера, дисциплина меток, согласование:
|
||
[ARCHITECTURE.md §13](ARCHITECTURE.md#13-container-lifecycle-in-process)
|
||
и `backend/docs/flows.md`.
|
||
|
||
---
|
||
|
||
## 7. Канал push
|
||
|
||
Раздел описывает, как платформа пушит real-time-события
|
||
аутентифицированным клиентам (turn-ready-сигналы, изменения
|
||
состояния лобби, инвалидации сессий).
|
||
|
||
### 7.1 Состав
|
||
|
||
В составе: gRPC-стрим, который клиент открывает к gateway,
|
||
bootstrap-событие, фрейминг форварднутых событий, control-канал
|
||
backend → gateway, который производит эти события.
|
||
|
||
Вне состава: каталог видов событий — см.
|
||
[Раздел 8](#8-уведомления-и-почта) для notification-стороны и
|
||
[`backend/README.md` §10](../backend/README.md#10-notification-catalog)
|
||
для закрытого списка.
|
||
|
||
### 7.2 Подписка клиента
|
||
|
||
Аутентифицированный клиент открывает server-streaming-вызов
|
||
`SubscribeEvents` на gateway. Gateway проводит ту же envelope-
|
||
проверку, что и для unary-запросов
|
||
([Раздел 1.4](#14-поиск-сессии-для-каждого-запроса)), затем
|
||
регистрирует стрим в своём внутреннем хабе. Первый фрейм,
|
||
получаемый клиентом — это gateway-подписанное bootstrap-событие
|
||
с текущим серверным временем, чтобы клиент мог калибровать свои
|
||
локальные часы без отдельного запроса.
|
||
|
||
### 7.3 Управление backend → gateway
|
||
|
||
Backend хостит единственный gRPC-сервис `Push.SubscribePush`,
|
||
потребляемый gateway. На каждую client-id gateway одновременно
|
||
существует ровно одна логическая подписка; переподключение с тем
|
||
же id заменяет старую подписку. Каждый фрейм в стриме несёт
|
||
монотонный курсор и одну из двух форм payload:
|
||
|
||
- **Client event.** Типизированный payload, адресованный одному
|
||
пользователю (и опционально одной устройства-сессии). Поставщики
|
||
передают в `push.Service` объект `push.Event` (Kind + Marshal),
|
||
сервис сам вызывает Marshal и кладёт байты в
|
||
`pushv1.ClientEvent.Payload`. Gateway форвардит байты внутри
|
||
подписанного клиентского конверта без переинтерпретации.
|
||
Поставщики прикрепляют correlation-id'ы, которые gateway
|
||
пробрасывает as-is. Новые виды событий поставляются с
|
||
FlatBuffers-реализацией Event; виды, ещё не мигрировавшие,
|
||
используют fallback `push.JSONEvent`, чтобы pipeline продолжал
|
||
отправлять их без задержек на миграцию.
|
||
- **Session invalidation.** Говорит gateway сбросить активные
|
||
стримы и отклонить in-flight-запросы для затронутых сессий —
|
||
путь распространения отзыва, описанный в
|
||
[Разделе 1.5](#15-отзыв).
|
||
|
||
### 7.4 Надёжность и переподключение
|
||
|
||
Backend держит in-memory ring-буфер недавних событий. При
|
||
переподключении gateway шлёт последний потреблённый курсор;
|
||
backend возобновляет с следующего события, если курсор всё ещё
|
||
внутри freshness-window-TTL, или начинает с головы, если курсор
|
||
устарел. Per-connection-обратное давление — drop-oldest:
|
||
медленное gateway-соединение теряет старые события первыми, при
|
||
каждом дропе пишется log-строка, чтобы обе стороны могли
|
||
скоррелировать дыру.
|
||
|
||
Push-канал — best-effort. Durable-запись "мы попытались сообщить
|
||
этому пользователю об этом событии" живёт в `notifications` /
|
||
`notification_routes` ([Раздел 8](#8-уведомления-и-почта));
|
||
потерянное push-событие не значит, что платформа забыла событие.
|
||
|
||
### 7.5 Поставщики
|
||
|
||
Backend-поставщики, эмитящие в push-канал, — это: notification-
|
||
диспатчер (push-маршруты из каталога) и session-модуль
|
||
(события отзыва). Никакой другой доменный модуль не эмитит
|
||
client-события, кроме notification-диспатчера.
|
||
|
||
### 7.6 Перекрёстные ссылки
|
||
|
||
- Wire-конверт, используемый для push-фреймов:
|
||
[ARCHITECTURE.md §15](ARCHITECTURE.md#15-transport-security-model-gateway-boundary).
|
||
- Семантика переподключения и ring-буфера:
|
||
[ARCHITECTURE.md §8](ARCHITECTURE.md#8-backend--gateway-communication)
|
||
и `backend/docs/flows.md` "Push gRPC".
|
||
- Notification-диспатчер: [Раздел 8](#8-уведомления-и-почта).
|
||
|
||
---
|
||
|
||
## 8. Уведомления и почта
|
||
|
||
Раздел описывает, как платформа сообщает пользователю о событии
|
||
через push или e-mail (или оба).
|
||
|
||
### 8.1 Состав
|
||
|
||
В составе: подача notification-намерения, веер по push- и email-
|
||
каналам, durable mail outbox, dead-letter-обработка, оператор-
|
||
инициированный resend.
|
||
|
||
Вне состава: per-event-семантика — когда срабатывает каждый вид,
|
||
описано в соответствующем feature-разделе
|
||
([Раздел 4](#4-участие-в-лобби) для lobby-видов,
|
||
[Раздел 5](#5-реестр-названий-рас) для race-name-видов,
|
||
[Раздел 6](#6-игровая-сессия) для game-видов).
|
||
|
||
### 8.2 Notification-намерение и веер
|
||
|
||
Доменные поставщики (lobby, runtime, geo) подают типизированное
|
||
намерение в notification-модуль вместо передачи сообщения в
|
||
конкретный канал. Модуль затем:
|
||
|
||
- enforced'ит идемпотентность по виду намерения плюс
|
||
идемпотентному ключу от поставщика;
|
||
- резолвит получателей;
|
||
- материализует один маршрут на получателя на канал, по политике
|
||
каталога, специфичной для типа (только push, только email,
|
||
оба, или admin email);
|
||
- эмитит push-маршруты в gRPC push-стрим, потребляемый gateway;
|
||
- вставляет email-маршруты прямо в mail outbox.
|
||
|
||
Малформ-намерения карантинизируются в выделенную таблицу и
|
||
никогда не блокируют поставщика.
|
||
|
||
### 8.3 Каталог
|
||
|
||
Каталог — это закрытый набор видов. Каждый вид специфицирует свои
|
||
каналы и payload-поля, потребляемые шаблонами и клиентами.
|
||
Три категории записей заслуживают отдельного упоминания:
|
||
|
||
- **`auth.login_code`.** Это единственный вид, обходящий
|
||
notification-pipeline целиком. Auth пишет email-строку прямо
|
||
в outbox, чтобы commit challenge был атомарен с mail-enqueue.
|
||
- **`runtime.*`-виды.** Они доставляются на сконфигурированный
|
||
admin email. Если admin email не сконфигурирован, маршруты
|
||
ложатся со статусом `skipped` и оператор-логом — запрос никогда
|
||
не падает из-за отсутствия operator-конфига.
|
||
- **Зарезервированные виды без поставщика.** `game.*` и
|
||
`mail.dead_lettered` перечислены в каталоге, но текущий код
|
||
никаких из них не эмитит. Добавление поставщика чисто аддитивно.
|
||
|
||
### 8.4 Mail outbox
|
||
|
||
Email — это Postgres-backed durable outbox. Поставщики
|
||
(notification-маршруты и auth login-code-путь) пишут delivery-
|
||
строку плюс рендеренные payload-байты в одной транзакции.
|
||
Worker-горутина дренит outbox: подбирает строки под row-lock,
|
||
пытается SMTP-доставку, записывает попытку и либо помечает строку
|
||
sent, либо планирует следующую попытку с exponential backoff
|
||
и jitter.
|
||
|
||
Доставка, превысившая бюджет попыток, переходит в dead-letter-
|
||
таблицу; сам dead-lettering эмитит admin-notification-намерение.
|
||
На старте worker дренит всё, что в pending или retrying — нет
|
||
отдельного recovery-флоу.
|
||
|
||
Операторы могут переслать non-`sent` доставку с admin-surface
|
||
([Раздел 10](#10-администрирование)). Resend по `sent`-доставке
|
||
отклоняется, чтобы оператор случайно не пере-отправил почту,
|
||
которая уже ушла из relay.
|
||
|
||
### 8.5 Видимость для оператора
|
||
|
||
Admin-surface перечисляет deliveries, attempts на delivery,
|
||
dead-letters, notifications, notification-dead-letters и malformed
|
||
notification-намерения. Ничего из этих листингов недоступно с
|
||
user-surface.
|
||
|
||
### 8.6 Перекрёстные ссылки
|
||
|
||
- Таблица notification-каталога (виды, каналы, payload):
|
||
[`backend/README.md` §10](../backend/README.md#10-notification-catalog).
|
||
- Внутренности mail outbox (таблицы, лог попыток, worker pickup):
|
||
[ARCHITECTURE.md §11](ARCHITECTURE.md#11-mail-outbox) и
|
||
`backend/docs/flows.md` "Mail outbox".
|
||
- Push-транспорт для client_event-маршрутов:
|
||
[Раздел 7](#7-канал-push).
|
||
|
||
---
|
||
|
||
## 9. Гео-сигнал
|
||
|
||
Раздел описывает, что backend записывает о source IP
|
||
аутентифицированного запроса и что он сознательно с этим
|
||
не делает.
|
||
|
||
### 9.1 Состав
|
||
|
||
В составе: one-shot declared_country при регистрации,
|
||
fire-and-forget per-request country-counter, оператор-only
|
||
read-endpoint.
|
||
|
||
Вне состава: любой автоматический flagging, обнаружение account-
|
||
takeover, geo-fencing, enforcement санкций, история версий.
|
||
Гео-сигнал — это пассивная запись, не enforcement-механизм.
|
||
|
||
### 9.2 Что backend записывает
|
||
|
||
При регистрации ([Раздел 1.3](#13-подтверждение-вызова)) backend
|
||
ищет source IP в GeoLite2-country-базе и сохраняет полученный
|
||
ISO-код страны на аккаунте. Это значение пишется ровно один раз
|
||
на аккаунт; последующие входы из другой страны не перезаписывают
|
||
его.
|
||
|
||
При каждом аутентифицированном запросе через user-surface
|
||
fire-and-forget-горутина выполняет тот же поиск против request
|
||
IP и инкрементирует per-(user, country)-счётчик. Сам запрос
|
||
никогда не блокируется этой работой; горутина запускается
|
||
после возврата handler'а.
|
||
|
||
Оба пути fail-open: ошибка geoip-поиска логируется, но никогда
|
||
не блокирует пользователя.
|
||
|
||
### 9.3 Что backend НЕ делает
|
||
|
||
- Никакой агрегации по пользователям.
|
||
- Никакого автоматического flagging при смене страны.
|
||
- Никаких уведомлений из geo-сигнала.
|
||
- Никакой истории версий `declared_country`.
|
||
- Никакой корреляции с санкциями, лимитами или entitlement.
|
||
|
||
### 9.4 Доступ оператора
|
||
|
||
Admin-surface предоставляет один read-endpoint, перечисляющий
|
||
per-user-country-счётчики. Данные предназначены для ручного
|
||
inspect'а во время оператор-triage; UI-флоу поверх этого нет.
|
||
|
||
### 9.5 Дисциплина source IP
|
||
|
||
Backend читает source IP из самого левого `X-Forwarded-For`-
|
||
entry, откатываясь на connection peer, если заголовок отсутствует.
|
||
Backend доверяет значению, потому что сетевой сегмент между
|
||
gateway и backend — это платформенный trust boundary, edge уже
|
||
саниТизировал его. Это сделано намеренно и переутверждено в
|
||
[ARCHITECTURE.md §10](ARCHITECTURE.md#10-geo-profile-reduced) и
|
||
[§16](ARCHITECTURE.md#16-security-boundaries-summary).
|
||
|
||
E-mail-адреса никогда не пишутся в логи как есть. Backend пишет
|
||
process-scoped HMAC-truncated hash, чтобы операторы могли
|
||
скоррелировать log-строки внутри одного process lifetime без
|
||
сохранения PII.
|
||
|
||
### 9.6 Перекрёстные ссылки
|
||
|
||
- Обоснование trust-boundary:
|
||
[ARCHITECTURE.md §10](ARCHITECTURE.md#10-geo-profile-reduced),
|
||
[§15](ARCHITECTURE.md#15-transport-security-model-gateway-boundary),
|
||
[§16](ARCHITECTURE.md#16-security-boundaries-summary).
|
||
- Контракт one-shot-write при регистрации vs per-request-counter:
|
||
[`backend/README.md` §11](../backend/README.md#11-geo-profile).
|
||
|
||
---
|
||
|
||
## 10. Администрирование
|
||
|
||
Раздел описывает каждую admin-only-операцию. Многие из них
|
||
упоминались в предыдущих разделах (admin-overrides для лобби,
|
||
admin-soft-delete и санкции, mail- и notification-inspection);
|
||
этот раздел — консолидированное вью.
|
||
|
||
### 10.1 Состав
|
||
|
||
В составе: admin-аутентификация, cross-domain-admin-операции,
|
||
их побочные эффекты на остальную часть платформы.
|
||
|
||
Вне состава: end-user-флоу, разделяющие домен с admin-операцией —
|
||
они в своём собственном разделе.
|
||
|
||
### 10.2 Аутентификация и bootstrap
|
||
|
||
Admin-surface использует HTTP Basic Auth против backend-owned
|
||
admin-account-таблицы; пароли хешированы bcrypt'ом. На старте,
|
||
если bootstrap-admin-username и пароль сконфигурированы и
|
||
таблица ещё не содержит строки с этим username, backend вставляет
|
||
её. Insert идемпотентен: последующие рестарты ничего не делают.
|
||
|
||
Failed Basic Auth-ответ запрашивает оператор-tooling за
|
||
credentials стандартным способом; realm-строка фиксирована,
|
||
чтобы password manager оператора мог матчить её через
|
||
deployments.
|
||
|
||
После первого деплоя bootstrap-пароль должен быть ротирован
|
||
через admin-surface.
|
||
|
||
### 10.3 Управление admin-аккаунтами
|
||
|
||
Существующие админы могут перечислять других админов, создавать
|
||
новых, искать конкретного, отключать или включать обратно
|
||
админа, и сбрасывать пароль. Отключённая admin-строка не может
|
||
аутентифицироваться; строка сохраняется, чтобы сохранить
|
||
audit-references, а не удаляется.
|
||
|
||
Reset-password принимает новый пароль в теле запроса. Backend
|
||
bcrypt-хеширует его, заменяет `admin_accounts.password_hash` и
|
||
возвращает обновлённую `AdminAccount`-форму — сам новый пароль
|
||
никогда не возвращается. "Delivered out-of-band" поэтому
|
||
означает: админ, инициирующий reset — тот, кто должен сообщить
|
||
новое значение target-админу через какой-то канал вне платформы
|
||
(защищённый мессенджер, голос и т.п.); платформа не email'ит
|
||
и не auto-доставляет.
|
||
|
||
### 10.4 Администрирование пользователей
|
||
|
||
Для любого user-аккаунта админ может:
|
||
|
||
- перечислять и инспектировать аккаунты;
|
||
- применить санкцию;
|
||
- применить per-user-limit-override, корректирующий конкретную
|
||
квоту;
|
||
- обновить entitlement (план, paid-флаг, source, validity);
|
||
- soft-delete'нуть аккаунт (тот же in-process-каскад, что и
|
||
[Раздел 2.4](#24-удаление-аккаунта-пользователем)).
|
||
|
||
Каталог санкций сознательно минимален в MVP: единственный
|
||
поддерживаемый `sanction_code` — это `permanent_block`. Применение
|
||
переключает `accounts.permanent_block`, отзывает все активные
|
||
сессии ([Раздел 1.5](#15-отзыв)) и запускает тот же lobby-каскад,
|
||
что и soft-delete, со membership-статусом `blocked`
|
||
([Раздел 2.4](#24-удаление-аккаунта-пользователем)). OpenAPI-схема
|
||
кодирует это как закрытый enum, чтобы будущие добавления были
|
||
явным breaking-изменением. Soft-delete всегда отзывает сессии;
|
||
санкции отзывают только когда вид документирует этот побочный
|
||
эффект (сегодня — только `permanent_block`).
|
||
|
||
### 10.5 Администрирование игр
|
||
|
||
Админы создают public-игры, перечисляют и инспектируют любую игру,
|
||
делают force-start или force-stop игре и баннят member'а.
|
||
Force-stop сносит запущенный engine-контейнер для игры; ban-member
|
||
добавляет пользователя в block-лист игры и удаляет любой
|
||
активный membership ([Раздел 4.4](#44-членства)).
|
||
|
||
Public-game-владение коллективное: строка несёт `owner_user_id IS
|
||
NULL` и любой админ может действовать с ней. User-surface никогда
|
||
не производит и не транзишнит public-игру.
|
||
|
||
### 10.6 Администрирование runtime
|
||
|
||
Админы инспектируют runtime-запись для игры, рестартят engine-
|
||
контейнер, патчат его image на более новый semver-патч в той же
|
||
major / minor-линии и форсят one-shot-доп-тик хода.
|
||
|
||
Patch сознательно ограничен patch-компонентом. Major- или minor-
|
||
смена версии требует явного stop / start игры, не in-place-апгрейда.
|
||
Регистрация версий движка и disable — рядом.
|
||
|
||
### 10.7 Реестр версий движка
|
||
|
||
Реестр версий движка — источник разрешённых engine-image'ов.
|
||
Поставщики (start, restart, patch) никогда не выбирают image-
|
||
references сами; они читают из реестра. Disable версии — forward-
|
||
looking-решение: существующие запущенные контейнеры держат свой
|
||
текущий image до stop / start, но disabled-версия больше не
|
||
eligible для новых стартов или патчей.
|
||
|
||
### 10.8 Администрирование почты и уведомлений
|
||
|
||
Операторы могут перечислять и инспектировать mail-deliveries,
|
||
attempts на delivery, dead-letters, notifications, notification-
|
||
dead-letters и malformed notification-намерения. Они также могут
|
||
переслать non-sent mail-delivery ([Раздел 8.4](#84-mail-outbox)).
|
||
|
||
Эти вью — единственный путь к видимости почты и уведомлений
|
||
вне телеметрии.
|
||
|
||
### 10.9 Администрирование geo
|
||
|
||
Единственный geo admin-endpoint перечисляет per-user-country-
|
||
счётчики ([Раздел 9.4](#94-доступ-оператора)). Admin-write-
|
||
доступа к geo-данным нет; declared_country устанавливается раз
|
||
при регистрации и не меняется, счётчики заполняются runtime'ом,
|
||
а операторы могут только читать.
|
||
|
||
### 10.10 Перекрёстные ссылки
|
||
|
||
- Контракт каскада soft-delete:
|
||
[ARCHITECTURE.md §7](ARCHITECTURE.md#7-in-process-async-patterns).
|
||
- Жизненный цикл контейнера и арбитраж версий:
|
||
[ARCHITECTURE.md §13](ARCHITECTURE.md#13-container-lifecycle-in-process).
|
||
- Mail outbox и notification-диспатчер:
|
||
[ARCHITECTURE.md §11](ARCHITECTURE.md#11-mail-outbox),
|
||
[§12](ARCHITECTURE.md#12-notification-pipeline) и
|
||
[Раздел 8](#8-уведомления-и-почта).
|