админка

This commit is contained in:
2026-03-13 23:33:35 +04:00
parent bae063a4fa
commit eaedeeb52b
41 changed files with 2506 additions and 67 deletions
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\UpdateAccountAccessRequest;
use App\Services\AzerothCoreAccountService;
use App\Services\InviteCodeService;
use App\Support\WowData;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class AccountController extends Controller
{
public function index(
Request $request,
AzerothCoreAccountService $accounts,
InviteCodeService $inviteCodes,
): View
{
$search = trim((string) $request->query('search', ''));
return view('admin.accounts.index', [
'accounts' => $accounts->listAccounts($search !== '' ? $search : null),
'inviteCodes' => $inviteCodes->latest(),
'inviteStats' => $inviteCodes->stats(),
'search' => $search,
'accessLevels' => WowData::securityLevels(),
]);
}
public function updateAccess(int $accountId, UpdateAccountAccessRequest $request, AzerothCoreAccountService $accounts): RedirectResponse
{
abort_unless($accounts->findUserById($accountId), 404);
$accounts->updateAccessLevel($accountId, (int) $request->input('gmlevel'));
return back()->with('status', 'Уровень доступа обновлён.');
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\CreateInviteCodesRequest;
use App\Services\InviteCodeService;
use Illuminate\Http\RedirectResponse;
class InviteCodeController extends Controller
{
public function store(CreateInviteCodesRequest $request, InviteCodeService $inviteCodes): RedirectResponse
{
$created = $inviteCodes->createCodes(
(int) $request->integer('quantity'),
$request->user(),
);
return back()->with('status', sprintf('Создано инвайт-кодов: %d.', $created->count()));
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class LoginController extends Controller
{
public function create(): View
{
return view('auth.login');
}
public function store(LoginRequest $request): RedirectResponse
{
if (! Auth::attempt($request->safe()->only(['username', 'password']))) {
return back()
->withErrors(['username' => 'Неверный логин или пароль.'])
->onlyInput('username');
}
$request->session()->regenerate();
return redirect()->intended(route('cabinet.index'));
}
public function destroy(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()
->route('login')
->with('status', 'Сеанс завершён. Можно войти снова.');
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ChangePasswordRequest;
use App\Services\AzerothCoreAccountService;
use App\Services\GameClientDownloadService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Throwable;
class CabinetController extends Controller
{
public function index(Request $request, AzerothCoreAccountService $accounts): View
{
$user = $request->user();
return view('cabinet.index', [
'user' => $user,
'characters' => $accounts->charactersForAccount($user->id),
'clientFilename' => config('moonwell.client.object_key'),
]);
}
public function updatePassword(ChangePasswordRequest $request, AzerothCoreAccountService $accounts): RedirectResponse
{
$user = $request->user();
if (! $accounts->validatePassword($user, $request->string('current_password')->toString())) {
return back()->withErrors(['current_password' => 'Текущий пароль указан неверно.']);
}
$accounts->updatePassword($user, $request->string('password')->toString());
return back()->with('status', 'Пароль успешно обновлён.');
}
public function client(GameClientDownloadService $clientDownloadService): RedirectResponse
{
try {
return redirect()->away($clientDownloadService->temporaryUrl());
} catch (Throwable $exception) {
report($exception);
return back()->with('error', 'Не удалось получить ссылку на клиент. Попробуй ещё раз позже.');
}
}
}
+20 -6
View File
@@ -3,8 +3,10 @@
namespace App\Http\Controllers;
use App\Exceptions\DuplicateGameAccountException;
use App\Exceptions\InviteCodeException;
use App\Http\Requests\RegisterGameAccountRequest;
use App\Services\AzerothCoreAccountRegistrar;
use App\Services\InviteCodeService;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
use Throwable;
@@ -85,24 +87,36 @@ class LandingController extends Controller
]);
}
public function register(RegisterGameAccountRequest $request, AzerothCoreAccountRegistrar $registrar): RedirectResponse
public function register(
RegisterGameAccountRequest $request,
AzerothCoreAccountRegistrar $registrar,
InviteCodeService $inviteCodes,
): RedirectResponse
{
try {
$result = $registrar->register(
$result = $inviteCodes->redeemForRegistration(
$request->string('invite_code')->toString(),
$request->string('username')->toString(),
$request->string('email')->toString(),
$request->string('password')->toString(),
fn (): array => $registrar->register(
$request->string('username')->toString(),
$request->string('email')->toString(),
$request->string('password')->toString(),
),
);
} catch (DuplicateGameAccountException $exception) {
return back()
->withErrors([$exception->field => $exception->getMessage()])
->withInput($request->safe()->only(['username', 'email']));
->withInput($request->safe()->only(['username', 'email', 'invite_code']));
} catch (InviteCodeException $exception) {
return back()
->withErrors([$exception->field => $exception->getMessage()])
->withInput($request->safe()->only(['username', 'email', 'invite_code']));
} catch (Throwable $exception) {
report($exception);
return back()
->with('registration_error', 'Не удалось создать аккаунт. Проверь настройки сервера и попробуй снова.')
->withInput($request->safe()->only(['username', 'email']));
->withInput($request->safe()->only(['username', 'email', 'invite_code']));
}
return redirect()
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureGameMaster
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
abort_unless(
$user && $user->canAccessAdminPanel(),
Response::HTTP_FORBIDDEN,
'Недостаточно прав для доступа к админке.',
);
return $next($request);
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ChangePasswordRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'current_password' => ['required', 'string', 'max:32'],
'password' => ['required', 'string', 'min:8', 'max:32', 'confirmed', 'regex:/^(?=.*[A-Za-z])(?=.*\d).+$/'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'current_password.required' => 'Укажи текущий пароль.',
'current_password.max' => 'Текущий пароль не должен быть длиннее 32 символов.',
'password.required' => 'Укажи новый пароль.',
'password.min' => 'Новый пароль должен содержать минимум 8 символов.',
'password.max' => 'Новый пароль не должен быть длиннее 32 символов.',
'password.confirmed' => 'Подтверждение нового пароля не совпадает.',
'password.regex' => 'Новый пароль должен содержать хотя бы одну букву и одну цифру.',
];
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CreateInviteCodesRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'quantity' => ['required', 'integer', 'min:1', 'max:50'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'quantity.required' => 'Укажи количество инвайт-кодов.',
'quantity.integer' => 'Количество должно быть числом.',
'quantity.min' => 'Нужно создать минимум один инвайт-код.',
'quantity.max' => 'За один раз можно создать не больше 50 инвайт-кодов.',
];
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'username' => ['required', 'string', 'min:3', 'max:32', 'regex:/^[A-Za-z0-9]+$/'],
'password' => ['required', 'string', 'min:3', 'max:32'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'username.required' => 'Укажи логин аккаунта.',
'username.min' => 'Логин должен содержать минимум 3 символа.',
'username.max' => 'Логин не должен быть длиннее 32 символов.',
'username.regex' => 'Логин может содержать только латинские буквы и цифры.',
'password.required' => 'Укажи пароль.',
'password.min' => 'Пароль должен содержать минимум 3 символа.',
'password.max' => 'Пароль не должен быть длиннее 32 символов.',
];
}
}
@@ -19,6 +19,7 @@ class RegisterGameAccountRequest extends FormRequest
return [
'username' => ['required', 'string', 'min:3', 'max:32', 'regex:/^[A-Za-z0-9]+$/'],
'email' => ['required', 'string', 'email:rfc', 'max:255'],
'invite_code' => ['required', 'string', 'min:6', 'max:32'],
'password' => ['required', 'string', 'min:8', 'max:32', 'confirmed', 'regex:/^(?=.*[A-Za-z])(?=.*\d).+$/'],
'terms' => ['accepted'],
];
@@ -39,6 +40,10 @@ class RegisterGameAccountRequest extends FormRequest
'email.string' => 'Email должен быть строкой.',
'email.email' => 'Укажи корректный email адрес.',
'email.max' => 'Email не должен быть длиннее 255 символов.',
'invite_code.required' => 'Укажи инвайт-код.',
'invite_code.string' => 'Инвайт-код должен быть строкой.',
'invite_code.min' => 'Инвайт-код выглядит слишком коротким.',
'invite_code.max' => 'Инвайт-код выглядит слишком длинным.',
'password.required' => 'Укажи пароль.',
'password.string' => 'Пароль должен быть строкой.',
'password.min' => 'Пароль должен содержать минимум 8 символов.',
@@ -57,6 +62,7 @@ class RegisterGameAccountRequest extends FormRequest
return [
'username' => 'логин аккаунта',
'email' => 'email',
'invite_code' => 'инвайт-код',
'password' => 'пароль',
'terms' => 'правила',
];
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateAccountAccessRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'gmlevel' => ['required', 'integer', Rule::in(array_keys(\App\Support\WowData::securityLevels()))],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'gmlevel.required' => 'Укажи уровень доступа.',
'gmlevel.integer' => 'Уровень доступа должен быть числом.',
'gmlevel.in' => 'Выбран недопустимый уровень доступа.',
];
}
}