доп фукнции апи
This commit is contained in:
@@ -124,6 +124,9 @@ MOONWELL_RATE_REPUTATION=x1
|
||||
MOONWELL_CLIENT_DISK=s3
|
||||
MOONWELL_CLIENT_OBJECT_KEY="World of Warcraft.zip"
|
||||
MOONWELL_CLIENT_URL_TTL=30
|
||||
MOONWELL_REALM_STATUS_HOST=play.moon-well.online
|
||||
MOONWELL_REALM_STATUS_TIMEOUT=0.5
|
||||
MOONWELL_REALM_STATUS_CACHE=15
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\BalanceService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class LauncherAccountController extends Controller
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/launcher/account',
|
||||
summary: 'Получить информацию об аккаунте',
|
||||
description: 'Возвращает имя и текущий баланс авторизованного игрового аккаунта.',
|
||||
tags: ['Launcher'],
|
||||
security: [['bearerAuth' => []]],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Информация об аккаунте',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'username', type: 'string', example: 'PLAYER'),
|
||||
new OA\Property(property: 'balance', type: 'string', example: '1250.00'),
|
||||
],
|
||||
),
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
description: 'Не авторизован',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Unauthenticated.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)]
|
||||
public function show(Request $request, BalanceService $balances): JsonResponse
|
||||
{
|
||||
$account = $request->user();
|
||||
|
||||
return response()->json([
|
||||
'username' => (string) $account->username,
|
||||
'balance' => $balances->balance((int) $account->id),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\LauncherRealmService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Throwable;
|
||||
|
||||
class LauncherRealmController extends Controller
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/launcher/realms',
|
||||
summary: 'Получить список реалмов и их статус',
|
||||
description: 'Возвращает реалмы из таблицы realmlist. Статус online означает, что игровой порт реалма доступен по TCP.',
|
||||
tags: ['Launcher'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Список реалмов',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(
|
||||
property: 'data',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'name', type: 'string', example: 'MoonWell'),
|
||||
new OA\Property(property: 'address', type: 'string', example: 'logon.moon-well.online'),
|
||||
new OA\Property(property: 'port', type: 'integer', example: 8085),
|
||||
new OA\Property(property: 'icon', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'flag', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'timezone', type: 'integer', example: 4),
|
||||
new OA\Property(property: 'population', type: 'number', format: 'float', example: 0.5),
|
||||
new OA\Property(property: 'online', type: 'boolean', example: true),
|
||||
new OA\Property(property: 'status', type: 'string', enum: ['online', 'offline'], example: 'online'),
|
||||
],
|
||||
),
|
||||
),
|
||||
new OA\Property(property: 'checked_at', type: 'string', format: 'date-time'),
|
||||
],
|
||||
),
|
||||
),
|
||||
new OA\Response(
|
||||
response: 503,
|
||||
description: 'Не удалось получить список реалмов',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Не удалось получить список реалмов.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
new OA\Response(response: 429, description: 'Слишком много запросов'),
|
||||
],
|
||||
)]
|
||||
public function index(LauncherRealmService $realms): JsonResponse
|
||||
{
|
||||
try {
|
||||
return response()->json([
|
||||
'data' => $realms->realms(),
|
||||
'checked_at' => now()->toIso8601String(),
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Не удалось получить список реалмов.',
|
||||
], 503);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Exceptions\DuplicateGameAccountException;
|
||||
use App\Exceptions\InviteCodeException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\RegisterGameAccountRequest;
|
||||
use App\Services\AzerothCoreAccountRegistrar;
|
||||
use App\Services\InviteCodeService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Throwable;
|
||||
|
||||
class LauncherRegistrationController extends Controller
|
||||
{
|
||||
#[OA\Post(
|
||||
path: '/api/launcher/register',
|
||||
summary: 'Зарегистрировать игровой аккаунт',
|
||||
description: 'Создаёт игровой аккаунт AzerothCore. Инвайт-код обязателен, если на сервере включена регистрация по приглашениям.',
|
||||
tags: ['Launcher Auth'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['username', 'email', 'password', 'password_confirmation', 'terms'],
|
||||
properties: [
|
||||
new OA\Property(property: 'username', type: 'string', minLength: 3, maxLength: 32, pattern: '^[A-Za-z0-9]+$', example: 'Player'),
|
||||
new OA\Property(property: 'email', type: 'string', format: 'email', maxLength: 255, example: 'player@example.com'),
|
||||
new OA\Property(property: 'invite_code', type: 'string', minLength: 6, maxLength: 32, nullable: true, example: 'ABCD-EFGH-IJKL'),
|
||||
new OA\Property(property: 'password', type: 'string', minLength: 8, maxLength: 32, example: 'Secret123'),
|
||||
new OA\Property(property: 'password_confirmation', type: 'string', minLength: 8, maxLength: 32, example: 'Secret123'),
|
||||
new OA\Property(property: 'terms', type: 'boolean', example: true, description: 'Согласие с правилами сервера и пользовательским соглашением'),
|
||||
],
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Аккаунт создан',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Игровой аккаунт успешно создан.'),
|
||||
new OA\Property(
|
||||
property: 'account',
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 77),
|
||||
new OA\Property(property: 'username', type: 'string', example: 'PLAYER'),
|
||||
],
|
||||
type: 'object',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
description: 'Ошибка валидации, занятый логин или неверный инвайт-код',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Такой игровой аккаунт уже существует.'),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'object',
|
||||
additionalProperties: new OA\AdditionalProperties(
|
||||
type: 'array',
|
||||
items: new OA\Items(type: 'string'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
new OA\Response(
|
||||
response: 500,
|
||||
description: 'Внутренняя ошибка регистрации',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Не удалось создать аккаунт. Попробуй снова позже.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
new OA\Response(response: 429, description: 'Слишком много запросов'),
|
||||
],
|
||||
)]
|
||||
public function store(
|
||||
RegisterGameAccountRequest $request,
|
||||
AzerothCoreAccountRegistrar $registrar,
|
||||
InviteCodeService $inviteCodes,
|
||||
): JsonResponse {
|
||||
try {
|
||||
$register = fn (): array => $registrar->register(
|
||||
$request->string('username')->toString(),
|
||||
$request->string('email')->toString(),
|
||||
$request->string('password')->toString(),
|
||||
);
|
||||
|
||||
$account = config('moonwell.registration.require_invite_code')
|
||||
? $inviteCodes->redeemForRegistration(
|
||||
$request->string('invite_code')->toString(),
|
||||
$request->string('username')->toString(),
|
||||
$register,
|
||||
)
|
||||
: $register();
|
||||
} catch (DuplicateGameAccountException|InviteCodeException $exception) {
|
||||
return response()->json([
|
||||
'message' => $exception->getMessage(),
|
||||
'errors' => [
|
||||
$exception->field => [$exception->getMessage()],
|
||||
],
|
||||
], 422);
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Не удалось создать аккаунт. Попробуй снова позже.',
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Игровой аккаунт успешно создан.',
|
||||
'account' => [
|
||||
'id' => $account['id'],
|
||||
'username' => $account['username'],
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ use OpenApi\Attributes as OA;
|
||||
#[OA\Info(
|
||||
version: '1.0.0',
|
||||
title: 'Moonwell Launcher API',
|
||||
description: 'API для лаунчера WoW-клиента Moonwell. Авторизация, получение манифеста файлов и скачивание обновлений.',
|
||||
description: 'API для лаунчера WoW-клиента Moonwell. Авторизация, новости, реалмы и их статусы, получение манифеста файлов и скачивание обновлений.',
|
||||
)]
|
||||
#[OA\SecurityScheme(
|
||||
securityScheme: 'bearerAuth',
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class LauncherRealmService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RealmStatusProbe $statusProbe,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, array<string, bool|float|int|string>>
|
||||
*/
|
||||
public function realms(): Collection
|
||||
{
|
||||
$cacheSeconds = max(0, (int) config('moonwell.launcher.realm_status_cache', 15));
|
||||
|
||||
if ($cacheSeconds === 0) {
|
||||
return $this->loadRealms();
|
||||
}
|
||||
|
||||
return Cache::remember(
|
||||
'launcher.realm-status',
|
||||
now()->addSeconds($cacheSeconds),
|
||||
fn (): Collection => $this->loadRealms(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, array<string, bool|float|int|string>>
|
||||
*/
|
||||
private function loadRealms(): Collection
|
||||
{
|
||||
return DB::connection(config('moonwell.auth_connection'))
|
||||
->table('realmlist')
|
||||
->orderBy('id')
|
||||
->get([
|
||||
'id',
|
||||
'name',
|
||||
'address',
|
||||
'port',
|
||||
'icon',
|
||||
'flag',
|
||||
'timezone',
|
||||
'population',
|
||||
])
|
||||
->map(function (object $realm): array {
|
||||
$probeAddress = trim((string) config('moonwell.launcher.realm_status_host'));
|
||||
$online = $this->statusProbe->isOnline(
|
||||
$probeAddress !== '' ? $probeAddress : (string) $realm->address,
|
||||
(int) $realm->port,
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => (int) $realm->id,
|
||||
'name' => (string) $realm->name,
|
||||
'address' => (string) $realm->address,
|
||||
'port' => (int) $realm->port,
|
||||
'icon' => (int) $realm->icon,
|
||||
'flag' => (int) $realm->flag,
|
||||
'timezone' => (int) $realm->timezone,
|
||||
'population' => (float) $realm->population,
|
||||
'online' => $online,
|
||||
'status' => $online ? 'online' : 'offline',
|
||||
];
|
||||
})
|
||||
->values();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class RealmStatusProbe
|
||||
{
|
||||
public function isOnline(string $address, int $port): bool
|
||||
{
|
||||
if ($address === '' || $port < 1 || $port > 65535) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = str_contains($address, ':') && ! str_starts_with($address, '[')
|
||||
? '['.$address.']'
|
||||
: $address;
|
||||
$timeout = max(0.1, min(3.0, (float) config('moonwell.launcher.realm_status_timeout', 0.5)));
|
||||
$socket = @stream_socket_client(
|
||||
'tcp://'.$host.':'.$port,
|
||||
$errorCode,
|
||||
$errorMessage,
|
||||
$timeout,
|
||||
STREAM_CLIENT_CONNECT,
|
||||
);
|
||||
|
||||
if ($socket === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fclose($socket);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,9 @@ return [
|
||||
'base_path' => env('MOONWELL_LAUNCHER_BASE_PATH', 'World of Warcraft'),
|
||||
'manifest_key' => env('MOONWELL_LAUNCHER_MANIFEST_KEY', 'manifest.json'),
|
||||
'download_ttl' => (int) env('MOONWELL_LAUNCHER_DOWNLOAD_TTL', 60),
|
||||
'realm_status_host' => env('MOONWELL_REALM_STATUS_HOST'),
|
||||
'realm_status_timeout' => (float) env('MOONWELL_REALM_STATUS_TIMEOUT', 0.5),
|
||||
'realm_status_cache' => (int) env('MOONWELL_REALM_STATUS_CACHE', 15),
|
||||
],
|
||||
|
||||
'realm' => [
|
||||
|
||||
+299
-5
@@ -2,10 +2,63 @@
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Moonwell Launcher API",
|
||||
"description": "API для лаунчера WoW-клиента Moonwell. Авторизация, получение манифеста файлов и скачивание обновлений.",
|
||||
"description": "API для лаунчера WoW-клиента Moonwell. Авторизация, новости, реалмы и их статусы, получение манифеста файлов и скачивание обновлений.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"/api/launcher/account": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Launcher"
|
||||
],
|
||||
"summary": "Получить информацию об аккаунте",
|
||||
"description": "Возвращает имя и текущий баланс авторизованного игрового аккаунта.",
|
||||
"operationId": "9a18d29372e8de08e1d24237580f19e5",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Информация об аккаунте",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string",
|
||||
"example": "PLAYER"
|
||||
},
|
||||
"balance": {
|
||||
"type": "string",
|
||||
"example": "1250.00"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Не авторизован",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Unauthenticated."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/launcher/login": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -425,6 +478,247 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/launcher/realms": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Launcher"
|
||||
],
|
||||
"summary": "Получить список реалмов и их статус",
|
||||
"description": "Возвращает реалмы из таблицы realmlist. Статус online означает, что игровой порт реалма доступен по TCP.",
|
||||
"operationId": "6d142b37a7b9a121b7ad02fd6405e054",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Список реалмов",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"example": 1
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "MoonWell"
|
||||
},
|
||||
"address": {
|
||||
"type": "string",
|
||||
"example": "logon.moon-well.online"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"example": 8085
|
||||
},
|
||||
"icon": {
|
||||
"type": "integer",
|
||||
"example": 1
|
||||
},
|
||||
"flag": {
|
||||
"type": "integer",
|
||||
"example": 0
|
||||
},
|
||||
"timezone": {
|
||||
"type": "integer",
|
||||
"example": 4
|
||||
},
|
||||
"population": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"example": 0.5
|
||||
},
|
||||
"online": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "online",
|
||||
"enum": [
|
||||
"online",
|
||||
"offline"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"checked_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Не удалось получить список реалмов",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Не удалось получить список реалмов."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Слишком много запросов"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/launcher/register": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Launcher Auth"
|
||||
],
|
||||
"summary": "Зарегистрировать игровой аккаунт",
|
||||
"description": "Создаёт игровой аккаунт AzerothCore. Инвайт-код обязателен, если на сервере включена регистрация по приглашениям.",
|
||||
"operationId": "bfc1dbd42cbcc339bc8a13dbf189c525",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"username",
|
||||
"email",
|
||||
"password",
|
||||
"password_confirmation",
|
||||
"terms"
|
||||
],
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9]+$",
|
||||
"example": "Player",
|
||||
"maxLength": 32,
|
||||
"minLength": 3
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"example": "player@example.com",
|
||||
"maxLength": 255
|
||||
},
|
||||
"invite_code": {
|
||||
"type": "string",
|
||||
"example": "ABCD-EFGH-IJKL",
|
||||
"nullable": true,
|
||||
"maxLength": 32,
|
||||
"minLength": 6
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"example": "Secret123",
|
||||
"maxLength": 32,
|
||||
"minLength": 8
|
||||
},
|
||||
"password_confirmation": {
|
||||
"type": "string",
|
||||
"example": "Secret123",
|
||||
"maxLength": 32,
|
||||
"minLength": 8
|
||||
},
|
||||
"terms": {
|
||||
"description": "Согласие с правилами сервера и пользовательским соглашением",
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Аккаунт создан",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Игровой аккаунт успешно создан."
|
||||
},
|
||||
"account": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"example": 77
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"example": "PLAYER"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Ошибка валидации, занятый логин или неверный инвайт-код",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Такой игровой аккаунт уже существует."
|
||||
},
|
||||
"errors": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Внутренняя ошибка регистрации",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Не удалось создать аккаунт. Попробуй снова позже."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Слишком много запросов"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -444,14 +738,14 @@
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Launcher Auth",
|
||||
"description": "Launcher Auth"
|
||||
},
|
||||
{
|
||||
"name": "Launcher",
|
||||
"description": "Launcher"
|
||||
},
|
||||
{
|
||||
"name": "Launcher Auth",
|
||||
"description": "Launcher Auth"
|
||||
},
|
||||
{
|
||||
"name": "Launcher Service",
|
||||
"description": "Launcher Service"
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\GameBalanceController;
|
||||
use App\Http\Controllers\Api\LauncherAccountController;
|
||||
use App\Http\Controllers\Api\LauncherAuthController;
|
||||
use App\Http\Controllers\Api\LauncherController;
|
||||
use App\Http\Controllers\Api\LauncherNewsController;
|
||||
use App\Http\Controllers\Api\LauncherRealmController;
|
||||
use App\Http\Controllers\Api\LauncherRegistrationController;
|
||||
use App\Http\Controllers\Api\RobokassaController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::post('launcher/login', [LauncherAuthController::class, 'login'])
|
||||
->middleware('throttle:10,1');
|
||||
Route::post('launcher/register', [LauncherRegistrationController::class, 'store'])
|
||||
->middleware('throttle:8,1');
|
||||
Route::get('launcher/realms', [LauncherRealmController::class, 'index'])
|
||||
->middleware('throttle:60,1');
|
||||
|
||||
Route::middleware('auth:api')->prefix('launcher')->group(function () {
|
||||
Route::get('account', [LauncherAccountController::class, 'show']);
|
||||
Route::get('manifest', [LauncherController::class, 'manifest']);
|
||||
Route::get('download/{path}', [LauncherController::class, 'download'])
|
||||
->where('path', '.*');
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\GameAccount;
|
||||
use App\Services\BalanceService;
|
||||
use Laravel\Passport\Passport;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LauncherAccountFeatureTest extends TestCase
|
||||
{
|
||||
public function test_authenticated_launcher_can_get_username_and_balance(): void
|
||||
{
|
||||
$account = new GameAccount;
|
||||
$account->id = 7;
|
||||
$account->username = 'PLAYERONE';
|
||||
Passport::actingAs($account);
|
||||
|
||||
$this->mock(BalanceService::class, function (MockInterface $mock): void {
|
||||
$mock->shouldReceive('balance')
|
||||
->once()
|
||||
->with(7)
|
||||
->andReturn('1250.00');
|
||||
});
|
||||
|
||||
$this->getJson('/api/launcher/account')
|
||||
->assertOk()
|
||||
->assertExactJson([
|
||||
'username' => 'PLAYERONE',
|
||||
'balance' => '1250.00',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_launcher_account_endpoint_requires_authentication(): void
|
||||
{
|
||||
$this->getJson('/api/launcher/account')
|
||||
->assertUnauthorized();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Services\RealmStatusProbe;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LauncherRealmsFeatureTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'moonwell.auth_connection' => 'azerothcore_auth',
|
||||
'moonwell.launcher.realm_status_cache' => 0,
|
||||
'moonwell.launcher.realm_status_host' => null,
|
||||
'database.connections.azerothcore_auth' => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
],
|
||||
]);
|
||||
DB::purge('azerothcore_auth');
|
||||
|
||||
Schema::connection('azerothcore_auth')->create('realmlist', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('id')->primary();
|
||||
$table->string('name');
|
||||
$table->string('address');
|
||||
$table->unsignedInteger('port');
|
||||
$table->unsignedInteger('icon')->default(0);
|
||||
$table->unsignedInteger('flag')->default(0);
|
||||
$table->unsignedInteger('timezone')->default(0);
|
||||
$table->float('population')->default(0);
|
||||
$table->unsignedInteger('gamebuild')->default(12340);
|
||||
});
|
||||
}
|
||||
|
||||
public function test_launcher_can_get_realms_and_their_status_without_authentication(): void
|
||||
{
|
||||
DB::connection('azerothcore_auth')->table('realmlist')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'name' => 'MoonWell',
|
||||
'address' => 'realm.moon-well.online',
|
||||
'port' => 8085,
|
||||
'icon' => 1,
|
||||
'flag' => 0,
|
||||
'timezone' => 4,
|
||||
'population' => 0.5,
|
||||
'gamebuild' => 12340,
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'name' => 'MoonWell PTR',
|
||||
'address' => 'ptr.moon-well.online',
|
||||
'port' => 8085,
|
||||
'icon' => 1,
|
||||
'flag' => 0,
|
||||
'timezone' => 4,
|
||||
'population' => 0,
|
||||
'gamebuild' => 12340,
|
||||
],
|
||||
]);
|
||||
$this->mock(RealmStatusProbe::class, function (MockInterface $mock): void {
|
||||
$mock->shouldReceive('isOnline')
|
||||
->once()
|
||||
->with('realm.moon-well.online', 8085)
|
||||
->andReturnTrue();
|
||||
$mock->shouldReceive('isOnline')
|
||||
->once()
|
||||
->with('ptr.moon-well.online', 8085)
|
||||
->andReturnFalse();
|
||||
});
|
||||
|
||||
$this->getJson('/api/launcher/realms')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.name', 'MoonWell')
|
||||
->assertJsonPath('data.0.online', true)
|
||||
->assertJsonPath('data.0.status', 'online')
|
||||
->assertJsonPath('data.1.status', 'offline')
|
||||
->assertJsonStructure(['data', 'checked_at']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Exceptions\DuplicateGameAccountException;
|
||||
use App\Services\AzerothCoreAccountRegistrar;
|
||||
use App\Services\InviteCodeService;
|
||||
use Closure;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LauncherRegistrationFeatureTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config(['moonwell.registration.require_invite_code' => true]);
|
||||
}
|
||||
|
||||
public function test_launcher_can_register_game_account(): void
|
||||
{
|
||||
$this->mock(InviteCodeService::class, function (MockInterface $mock): void {
|
||||
$mock->shouldReceive('redeemForRegistration')
|
||||
->once()
|
||||
->with('INVITE-1234', 'playerone', \Mockery::type(Closure::class))
|
||||
->andReturnUsing(fn (string $code, string $username, Closure $callback): array => $callback());
|
||||
});
|
||||
$this->mock(AzerothCoreAccountRegistrar::class, function (MockInterface $mock): void {
|
||||
$mock->shouldReceive('register')
|
||||
->once()
|
||||
->with('playerone', 'player@example.com', 'Pass1234')
|
||||
->andReturn(['id' => 77, 'username' => 'PLAYERONE']);
|
||||
});
|
||||
|
||||
$this->postJson('/api/launcher/register', [
|
||||
'username' => 'playerone',
|
||||
'email' => 'player@example.com',
|
||||
'invite_code' => 'INVITE-1234',
|
||||
'password' => 'Pass1234',
|
||||
'password_confirmation' => 'Pass1234',
|
||||
'terms' => true,
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJson([
|
||||
'message' => 'Игровой аккаунт успешно создан.',
|
||||
'account' => [
|
||||
'id' => 77,
|
||||
'username' => 'PLAYERONE',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_launcher_registration_returns_validation_errors_as_json(): void
|
||||
{
|
||||
$this->postJson('/api/launcher/register', [
|
||||
'username' => 'игрок!',
|
||||
'email' => 'invalid',
|
||||
'password' => '123',
|
||||
'password_confirmation' => '456',
|
||||
'terms' => false,
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors([
|
||||
'username',
|
||||
'email',
|
||||
'invite_code',
|
||||
'password',
|
||||
'terms',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_launcher_registration_reports_duplicate_username(): void
|
||||
{
|
||||
$this->mock(InviteCodeService::class, function (MockInterface $mock): void {
|
||||
$mock->shouldReceive('redeemForRegistration')
|
||||
->once()
|
||||
->andReturnUsing(fn (string $code, string $username, Closure $callback): array => $callback());
|
||||
});
|
||||
$this->mock(AzerothCoreAccountRegistrar::class, function (MockInterface $mock): void {
|
||||
$mock->shouldReceive('register')
|
||||
->once()
|
||||
->andThrow(DuplicateGameAccountException::forUsername());
|
||||
});
|
||||
|
||||
$this->postJson('/api/launcher/register', [
|
||||
'username' => 'playerone',
|
||||
'email' => 'player@example.com',
|
||||
'invite_code' => 'INVITE-1234',
|
||||
'password' => 'Pass1234',
|
||||
'password_confirmation' => 'Pass1234',
|
||||
'terms' => true,
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonPath('errors.username.0', 'Такой игровой аккаунт уже существует.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user