339 lines
10 KiB
Markdown
339 lines
10 KiB
Markdown
# 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\"}"}
|
||
```
|