Инициализация Laravel и Admiral. Авторизация

Время прочтения — 10 минут
Содержание
Большинство админок на Laravel выглядят одинаково: авторизация, маршруты, пара форм, таблица. Но каждый раз приходится писать одно и то же — и тратить часы не на логику продукта, а на создание инфраструктуры, значительная часть которой повторяется из проекта в проект. Особенно раздражает, когда фронт и бэкенд приходится склеивать вручную: настраивать авторизацию, конфиги, структуру.
В какой-то момент мы устали от этой рутины и собрали Admiral – опенсорс-фреймворк для разработки админ-панелей, который избавляет от лишнего кода и задаёт единый стандарт. Он быстро стартует, легко расширяется и безболезненно интегрируется с Laravel.
⚡ Познакомиться с Admiral можно здесь: https://github.com/dev-family/admiral
В этой статье я покажу, как с нуля развернуть полноценную админку на Laravel + Admiral, подключить авторизацию через Sanctum и выйти на уровень, когда можно фокусироваться не на структуре, а на логике продукта.

Установка Laravel

Начнём с создания основной директории проекта — admiral-laravel-init. Переходим в неё.
Далее установим актуальную версию Laravel – 12. В директории admiral-laravel-init запускаем установку по инструкции из документации – composer global require laravel/installer.
Теперь выполним команду laravel-new-backend, где backend – наша директория с Laravel. В качестве базы данных выберем самый простой вариант – SQLite (но вы можете использовать любую, которая вам удобна).
Переходим в папку backend и запускаем наш сервер на Laravel с помощью команды: composer run dev.
В консоли появится APP_URL, в моем случае это – APP_URL: http://localhost:8000.
Переходим по ссылке и убеждаемся, что всё работает.

Установка Admiral

Чтобы инициализировать админку, запускаем команду: npx create-admiral-app@latest
В процессе установки выбираем Install the template without backend setting, затем в project name вводим название admin.
Теперь админка установлена по пути admiral-laravel-init/admin. Переходим в папку admin устанавливаем зависимости с помощью команды: npm i .
После этого редактируем файл .env – указываем адрес бэкенда Laravel. В моём случае это: VITE_API_URL=http://localhost:8000/admin.
Теперь можно запускать админку через выполнение команды: npm run build && npm run dev.
В консоли появится сообщение об успешном запуске. В моём случае это: Local: http://localhost:3000/.
Если перейти по этому адресу, произойдет редирект на страницу /login.

Авторизация

Теперь, когда у нас подняты и Admiral, и Laravel, можно приступить к разработке простой авторизации. Мы будем опираться на AuthProvider из документации Admiral и реализуем для него необходимые методы. Ознакомиться с ними можно по ссылке: https://github.com/dev-family/admiral?tab=readme-ov-file#auth---authprovider.
Прежде чем начать, сделаем несколько предварительных настроек. Для авторизации используем готовое решение от Laravel — Sanctum. Устанавливаем его командой: php artisan install:api.
Затем в файле config/auth.php добавляем новый guard с именем admin, указав драйвер sanctum.
 
'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'admin' => [
            'driver' => 'sanctum',
            'provider' => 'users',
        ],
    ],
Также в модель User.php добавим трейт HasApiTokens – он отвечает за работу с токенами API и необходим для корректной работы Sanctum.

class User extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\UserFactory> */
    use HasFactory, Notifiable, HasApiTokens;
Теперь можно переходить к созданию контроллера AuthController со следующим содержимым.

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,
        ];
    }
}
Для удобства также создадим соответствующие классы Request и Resource.

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);
    }
}
Теперь перейдём непосредственно к созданию сервиса авторизации. Предлагаем организовать структуру следующим образом: services → admin → auth. Вы можете выбрать любую другую, но, на наш взгляд, такой подход более оптимален – он следует принципу разделения ответственности: логику, связанную с админкой, удобно выносить в отдельные директории.
То же самое касается и ранее созданных файлов – при желании их также можно структурировать аналогичным образом.
Далее создадим два файла.

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 – в итоге получится путь 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();
После этого добавим сид для первого пользователя. Переходим в database/seeders и обновляем файл 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 и добавьте в него маршрут /admin. Должно получиться примерно так: 'paths' => ['api/*', 'sanctum/csrf-cookie', 'admin/*'],
В результате, после успешной авторизации, вы должны попасть в админ-панель.
Поздравляем! Вы успешно подняли Admiral и подключили авторизацию. Теперь можно приступать к разработке различных CRUD-операций – но это уже тема для отдельной статьи.
Остались вопросы по процессу инициализации? Отправьте их нам
Максим Бонцевич
CEO dev.family
Читайте также