доп фукнции апи

This commit is contained in:
2026-07-28 20:42:23 +04:00
parent c007a755fe
commit 6c18cad816
13 changed files with 894 additions and 6 deletions
@@ -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);
}
}
+1 -1
View File
@@ -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',
+74
View File
@@ -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();
}
}
+33
View File
@@ -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;
}
}