# Sea Battle — Main Server API Центральный сервер для браузерной онлайн-игры «Морской бой». Предоставляет REST API для регистрации, аутентификации и матчмейкинга. ## Базовый URL ``` http://localhost:3000/api ``` ## Аутентификация Все endpoints (кроме `/register` и `/login`) требуют JWT-токен в заголовке: ``` Authorization: Bearer ``` Токен выдаётся при регистрации или входе и действителен **7 дней**. --- ## Endpoints ### Регистрация Создаёт нового пользователя и возвращает токен. **URL:** `POST /api/register` **Auth:** не требуется **Content-Type:** `application/json` или `multipart/form-data` (при загрузке аватара) **Параметры (JSON):** | Параметр | Тип | Обязательный | Описание | |----------|-----|--------------|----------| | `user.nickname` | string | ✅ | Уникальный никнейм (3–24 символа) | | `user.password` | string | ✅ | Пароль (минимум 6 символов) | | `user.password_confirmation` | string | ✅ | Подтверждение пароля | | `user.avatar` | file | — | Аватар (PNG, JPEG, GIF, WebP) | **Пример запроса (JSON):** ```bash curl -X POST http://localhost:3000/api/register \ -H "Content-Type: application/json" \ -d '{ "user": { "nickname": "CaptainNemo", "password": "secret123", "password_confirmation": "secret123" } }' ``` **Пример запроса (с аватаром):** ```bash curl -X POST http://localhost:3000/api/register \ -F "user[nickname]=CaptainNemo" \ -F "user[password]=secret123" \ -F "user[password_confirmation]=secret123" \ -F "user[avatar]=@avatar.png" ``` **Успешный ответ (201 Created):** ```json { "user": { "id": 1, "nickname": "CaptainNemo", "avatar_url": "http://localhost:3000/rails/active_storage/blobs/...", "created_at": "2026-09-21T10:00:00.000Z" }, "token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjox..." } ``` **Ошибки (422 Unprocessable Entity):** ```json { "errors": [ "Nickname has already been taken", "Password is too short (minimum is 6 characters)" ] } ``` --- ### Вход Аутентифицирует пользователя и возвращает токен. **URL:** `POST /api/login` **Auth:** не требуется **Content-Type:** `application/json` **Параметры:** | Параметр | Тип | Обязательный | Описание | |----------|-----|--------------|----------| | `nickname` | string | ✅ | Никнейм | | `password` | string | ✅ | Пароль | **Пример запроса:** ```bash curl -X POST http://localhost:3000/api/login \ -H "Content-Type: application/json" \ -d '{ "nickname": "CaptainNemo", "password": "secret123" }' ``` **Успешный ответ (200 OK):** ```json { "user": { "id": 1, "nickname": "CaptainNemo", "avatar_url": "http://localhost:3000/rails/active_storage/blobs/...", "created_at": "2026-09-21T10:00:00.000Z" }, "token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjox..." } ``` **Ошибка (401 Unauthorized):** ```json { "error": "Invalid nickname or password" } ``` --- ### Текущий пользователь Возвращает данные авторизованного пользователя. **URL:** `GET /api/me` **Auth:** требуется **Пример запроса:** ```bash curl -X GET http://localhost:3000/api/me \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." ``` **Успешный ответ (200 OK):** ```json { "user": { "id": 1, "nickname": "CaptainNemo", "avatar_url": "http://localhost:3000/rails/active_storage/blobs/...", "created_at": "2026-09-21T10:00:00.000Z" } } ``` **Ошибка (401 Unauthorized):** ```json { "error": "Unauthorized" } ``` --- ### Выход Завершает сессию. JWT является stateless — клиент должен удалить токен. **URL:** `DELETE /api/logout` **Auth:** требуется **Пример запроса:** ```bash curl -X DELETE http://localhost:3000/api/logout \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." ``` **Успешный ответ (200 OK):** ```json { "message": "Signed out successfully" } ``` --- ### Поиск соперника (Матчмейкинг) Поиск соперника происходит через **WebSocket** (`Action Cable`), а не через long-polling: HTTP-поток не блокируется, а результат доставляется сервером push-уведомлением, как только найден соперник. **URL подключения:** `ws://localhost:3000/cable?token=` **Auth:** токен передаётся параметром `token` в query string (тот же JWT, что и для HTTP API). **Канал:** `MatchmakingChannel` #### Поток взаимодействия 1. Подключиться к `/cable?token=...` и подписаться на канал `MatchmakingChannel`. 2. Отправить команду начала поиска: ```json { "action": "start", "rating": 1000 } ``` Поле `rating` опционально (по умолчанию — `MATCHMAKING_DEFAULT_RATING`). Игроки подбираются по близости рейтинга; допуск расширяется с ростом времени ожидания. 3. Дождаться одного из сообщений: **Матч найден:** ```json { "type": "matched", "match_id": "145575d6-3a4b-4c5d-9e6f-7a8b9c0d1e2f", "game_token": "eyJhbGciOiJIUzI1NiJ9.eyJtYXRjaF9pZCI6IjE...", "game_server": "ws://localhost:8080", "players": [1, 2] } ``` | Поле | Описание | |------|----------| | `match_id` | Уникальный идентификатор матча (UUID) | | `game_token` | JWT-токен для аутентификации на игровом сервере | | `game_server` | WebSocket-адрес игрового сервера | | `players` | ID обоих игроков `[player1_id, player2_id]` | **Таймаут поиска** (соперник не найден за `MATCHMAKING_QUEUE_TTL` секунд): ```json { "type": "timeout" } ``` 4. Поиск отменяется автоматически при закрытии WebSocket-подписки. Также можно явно отправить: ```json { "action": "stop" } ``` **Примечание:** `game_token` содержит `match_id`, `player1_id`, `player2_id`, `game_server` и действителен `MATCHMAKING_TOKEN_TTL` секунд. --- ### Отмена поиска Убирает игрока из очереди матчмейкинга. **URL:** `DELETE /api/matchmaking` **Auth:** требуется **Пример запроса:** ```bash curl -X DELETE http://localhost:3000/api/matchmaking \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." ``` **Успешный ответ (200 OK):** ```json { "message": "Matchmaking cancelled" } ``` --- ## Health Check Проверка доступности сервера. **URL:** `GET /up` **Auth:** не требуется **Успешный ответ (200 OK):** ```html ``` --- ## Коды ошибок | Код | Описание | |-----|----------| | `200` | Успешный запрос | | `201` | Ресурс создан (регистрация) | | `401` | Неавторизован (отсутствует/невалиден токен) | | `422` | Ошибка валидации | | `500` | Внутренняя ошибка сервера | --- ## Переменные окружения | Переменная | По умолчанию | Описание | |------------|--------------|----------| | `REDIS_URL` | `redis://localhost:6380/0` | Подключение к Redis (матчмейкинг и pub/sub Action Cable) | | `GAME_SERVER_URL` | `ws://localhost:8080` | Адрес игрового сервера для матчмейкинга | | `MATCHMAKING_DEFAULT_RATING` | `1000` | Рейтинг по умолчанию для новых игроков | | `MATCHMAKING_QUEUE_TTL` | `60` | Время ожидания соперника до таймаута, сек | | `MATCHMAKING_PENDING_TTL` | `15` | Время ожидания финализации пары, сек | | `MATCHMAKING_MATCH_TTL` | `120` | Время жизни данных матча, сек | | `MATCHMAKING_TOKEN_TTL` | `120` | Время жизни `game_token`, сек | | `MATCHMAKING_BASE_RANGE` | `100` | Начальный допуск по рейтингу | | `MATCHMAKING_RANGE_PER_SECOND` | `50` | Рост допуска за секунду ожидания | | `MATCHMAKING_MAX_RANGE` | `1000` | Максимальный допуск по рейтингу | | `MATCHMAKING_SWEEP_INTERVAL` | `2` | Периодичность фоновой сверки очереди, сек | | `CORS_ORIGINS` | `*` | Разрешённые origins для CORS (через запятую) | | `SECRET_KEY_BASE` | из `credentials.yml.enc` | Секретный ключ для JWT | --- ## Пример workflow: от регистрации до матча ```bash # 1. Регистрация TOKEN=$(curl -s -X POST http://localhost:3000/api/register \ -H "Content-Type: application/json" \ -d '{"user":{"nickname":"Player1","password":"secret123","password_confirmation":"secret123"}}' \ | jq -r '.token') # 2. Проверка профиля curl -s http://localhost:3000/api/me \ -H "Authorization: Bearer $TOKEN" # 3. Поиск матча — WebSocket (например, через wscat): # wscat -c "ws://localhost:3000/cable?token=$TOKEN" # > {"command":"subscribe","identifier":"{\"channel\":\"MatchmakingChannel\"}"} # > {"command":"message","identifier":"{\"channel\":\"MatchmakingChannel\"}","data":"{\"action\":\"start\",\"rating\":1000}"} ```