Files
ruby-sea-battle/main-server/API.md
T

339 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sea Battle — Main Server API
Центральный сервер для браузерной онлайн-игры «Морской бой». Предоставляет REST API для регистрации, аутентификации и матчмейкинга.
## Базовый URL
```
http://localhost:3000/api
```
## Аутентификация
Все endpoints (кроме `/register` и `/login`) требуют JWT-токен в заголовке:
```
Authorization: Bearer <token>
```
Токен выдаётся при регистрации или входе и действителен **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=<JWT>`
**Auth:** токен передаётся параметром `token` в query string (тот же JWT, что и для HTTP API).
**Канал:** `MatchmakingChannel`
#### Поток взаимодействия
1. Подключиться к `/cable?token=...` и подписаться на канал `MatchmakingChannel`.
2. Отправить команду начала поиска:
```json
{ "action": "start" }
```
Рейтинг для подбора берётся из базы данных (`users.rating`). У новых пользователей он инициализируется нейтральным значением `100` (значение задано в БД); в будущем рейтинг будет пересчитываться по результатам матчей. Игроки подбираются по близости рейтинга; допуск расширяется с ростом времени ожидания.
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` секунд.
---
### Отмена поиска
Отмена поиска происходит через WebSocket: достаточно отключиться от `MatchmakingChannel` либо отправить `{ "action": "stop" }`.
---
## Health Check
Проверка доступности сервера.
**URL:** `GET /up`
**Auth:** не требуется
**Успешный ответ (200 OK):**
```html
<!DOCTYPE html><html><body style="background-color: green"></body></html>
```
---
## Коды ошибок
| Код | Описание |
|-----|----------|
| `200` | Успешный запрос |
| `201` | Ресурс создан (регистрация) |
| `401` | Неавторизован (отсутствует/невалиден токен) |
| `422` | Ошибка валидации |
| `500` | Внутренняя ошибка сервера |
---
## Переменные окружения
| Переменная | По умолчанию | Описание |
|------------|--------------|----------|
| `REDIS_URL` | `redis://localhost:6380/0` | Подключение к Redis (матчмейкинг и pub/sub Action Cable) |
| `GAME_SERVER_URL` | `ws://localhost:8080` | Адрес игрового сервера для матчмейкинга |
| `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\"}"}
```