Если ты в 2025 году ты всё ещё руками собираешь авторизацию на Laravel для своей админки — у меня для тебя плохие новости. Ты либо получаешь удовольствие от страданий (либо просто не знаешь про Admiral).
А если нет желания быть заложником бесконечного копипаста и конфигурационного ада, то есть один очень простой способ перестать тратить часы на одни и те же ритуалы.
В этом гайде я покажу, как за пару движений поставить работающую связку Laravel + фронт + нормальную авторизацию и забыть про вечный треш с настройками.
Установка Laravel
Создаём основную директорию проекта:
mkdir admiral-laravel-init && cd admiral-laravel-init
Ставим актуальную версию Laravel — 12:
composer global require laravel/installer
Теперь создаём проект:
laravel new backend
Для базы данных берём SQLite (но вы можете выбрать любую, если вам скучно жить).
Заходим в папку и запускаем сервер:
cd backend composer run dev
В консоли появится:
APP_URL: http://localhost:8000
Переходим по ссылке и убеждаемся, что Laravel жив.
Установка Admiral
Чтобы поднять админку, запускаем:
npx create-admiral-app@latest
Выбираем Install the template without backend setting, а в project name вводим admin.
Дальше:
cd admin npm i
В .env указываем адрес бэкенда Laravel:
VITE_API_URL=http://localhost:8000/admin
Запускаем админку:
npm run build && npm run dev
В консоли появится что-то вроде:
Local: http://localhost:3000/
Заходим по адресу — вас сразу перекинет на /login.
Авторизация
Теперь, когда Laravel и Admiral подняты, займёмся авторизацией. Используем Sanctum:
php artisan install:api
Правим config/auth.php, добавляем новый guard admin:
'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'admin' => [ 'driver' => 'sanctum', 'provider' => 'users', ], ],
В модель User.php добавляем трейт HasApiTokens — он нужен для работы с API-токенами.
AuthController.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Http\Requests\LoginRequest; use App\Services\Admin\Auth\AuthService; use Illuminate\Validation\ValidationException; use App\Http\Resources\AuthUserResource; use App\Services\Admin\Auth\LimitLoginAttempts; class AuthController { use LimitLoginAttempts; public function __construct( private readonly AuthService $auth, ) { } public function getIdentity(Request $request): array { $user = $request->user(); return [ 'user' => AuthUserResource::make($user), ]; } public function checkAuth(Request $request): \Illuminate\Http\JsonResponse { return response()->json('ok', 200); } public function logout(Request $request): void { $request->user()->currentAccessToken()->delete(); } public function login(LoginRequest $request): array { if ($this->hasTooManyLoginAttempts($request)) { $this->fireLockoutEvent($request); $this->sendLockoutResponse($request); } try { $user = $this->auth->login($request->email(), $request->password()); } catch (ValidationException $e) { $this->incrementLoginAttempts($request); throw $e; } catch (\Throwable $e) { $this->incrementLoginAttempts($request); throw ValidationException::withMessages([ 'email' => [__('auth.failed')], ]); } $token = $user->createToken('admin'); return [ 'user' => AuthUserResource::make($user), 'token' => $token->plainTextToken, ]; } }
LoginRequest.php
<?php declare(strict_types=1); namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; final class LoginRequest extends FormRequest { public function rules(): array { return [ 'email' => [ 'required', 'email', ], 'password' => [ 'required', ], ]; } public function email(): string { return $this->input('email'); } public function password(): string { return $this->input('password'); } }
AuthUserResource.php
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class AuthUserResource extends JsonResource { public function toArray($request): array { $this->resource = [ 'id' => $this->resource->id, 'name' => $this->resource->name, 'email' => $this->resource->email, ]; return parent::toArray($request); } }
AuthService.php
<?php declare(strict_types = 1); namespace App\Services\Admin\Auth; use App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; final class AuthService { public function __construct() { } /** * @throws \Throwable */ public function login(string $email, string $password): User { $user = $this->findByEmail($email); throw_if( !$user || !Hash::check($password, $user->password), ValidationException::withMessages([ 'password' => __('auth.failed'), ]) ); return $user; } public function findByEmail(string $email): User|null { /** @var \App\Models\User $user */ $user = User::query() ->where('email', $email) ->first(); if (!$user) { return null; } return $user; } }
LimitLoginAttempts.php
<?php declare(strict_types=1); namespace App\Services\Admin\Auth; use Illuminate\Auth\Events\Lockout; use Illuminate\Cache\RateLimiter; use Illuminate\Http\Request; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Symfony\Component\HttpFoundation\Response; trait LimitLoginAttempts { public function maxAttempts(): int { return property_exists($this, 'maxAttempts') ? $this->maxAttempts : 5; } public function decayMinutes(): int { return property_exists($this, 'decayMinutes') ? $this->decayMinutes : 1; } protected function hasTooManyLoginAttempts(Request $request): bool { return $this->limiter()->tooManyAttempts( $this->throttleKey($request), $this->maxAttempts() ); } protected function incrementLoginAttempts(Request $request): void { $this->limiter()->hit( $this->throttleKey($request), $this->decayMinutes() * 60 ); } protected function sendLockoutResponse(Request $request): void { $seconds = $this->limiter()->availableIn( $this->throttleKey($request) ); throw ValidationException::withMessages([ $this->loginKey() => [__('auth.throttle', [ 'seconds' => $seconds, 'minutes' => ceil($seconds / 60), ])], ])->status(Response::HTTP_TOO_MANY_REQUESTS); } protected function clearLoginAttempts(Request $request): void { $this->limiter()->clear($this->throttleKey($request)); } protected function limiter(): RateLimiter { return app(RateLimiter::class); } protected function fireLockoutEvent(Request $request): void { event(new Lockout($request)); } protected function throttleKey(Request $request): string { return Str::transliterate(Str::lower($request->input($this->loginKey())) . '|' . $request->ip()); } protected function loginKey(): string { return 'email'; } }
Роутинг
routes/admin.php:
<?php declare(strict_types = 1); use Illuminate\Support\Facades\Route; use App\Http\Controllers\AuthController; Route::group(['prefix' => 'auth',], function () { Route::post('login', [AuthController::class, 'login'])->name('login'); Route::group(['middleware' => ['auth:admin']], function () { Route::post('logout', [AuthController::class, 'logout']); Route::get('/get-identity', [AuthController::class, 'getIdentity']); Route::get('/check-auth', [AuthController::class, 'checkAuth']); }); });
Подключение в bootstrap/app.php:
<?php use Illuminate\Support\Facades\Route; use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Routing\Middleware\SubstituteBindings; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( using: function () { Route::middleware('admin') ->prefix('admin') ->group(base_path('routes/admin.php')); Route::middleware('api') ->prefix('api') ->group(base_path('routes/api.php')); }, api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php' ) ->withMiddleware(function (Middleware $middleware): void { $middleware->group('admin', [SubstituteBindings::class]); }) ->withExceptions(function (Exceptions $exceptions): void { // })->create();
Сид первого пользователя
DatabaseSeeder.php:
<?php namespace Database\Seeders; use App\Models\User; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. */ public function run(): void { // User::factory(10)->create(); User::factory()->create([ 'name' => 'Test User', 'email' => 'test@example.com', 'password' => '12345678', ]); } }
Выполняем:
php artisan db:seed composer run dev
Если CORS решил испортить день
php artisan config:publish cors
В config/cors.php в paths добавляем:
'paths' => ['api/*', 'sanctum/csrf-cookie', 'admin/*'],
Итог
Запускай бекенд, фронт — и залогинься. Если что-то не работает, погугли CORS или попробуй мой лайфхак с config/cors.php.
Вот и вся магия. Патчи, костыли и лишние библиотеки — в топку.
Если хочешь прокачать CRUD или вписать роли — пиши, расскажу, как сделать круто и быстро.
Вот так просто можно перестать страдать с авторизацией и заняться нормальным кодом.
Мнения?
