Bitcoin

Bootstrapping Laravel + Admiral: Auth Without the Boilerplate

You know the drill. Spin up Laravel, glue on a frontend, duct-tape together some authentication, and pretend the repetition isn’t driving you insane. Most admin panels are the same—auth, a few routes, a form or two, maybe a table. And yet, somehow, I always catch myself wasting half a day rebuilding the same damn scaffolding I built last week.

That’s what pushed me to build Admiral — an open-source admin panel boilerplate that plays nicely with Laravel and skips the tedium. You can check it out here, but what I really want to do is walk you through a real-world setup: Laravel + Admiral with authentication using Sanctum. Minimal ceremony, just a working setup that gets out of your way so you can ship features.

Step 1: Installing Laravel

I started by creating a new project folder:

mkdir admiral-laravel-init && cd admiral-laravel-init

Next, I installed Laravel globally:

composer global require laravel/installer

Then I created a new Laravel app in a backend directory.

I went with SQLite for simplicity, but feel free to use MySQL, Postgres, or whatever suits you.

To verify things are working, I ran:

cd backend && composer run dev

Once the dev server starts, it prints the APP_URL. For me, it was:

APP_URL: http://localhost:8000

Opening that in a browser confirmed Laravel was up and running.

Step 2: Installing Admiral

To bootstrap the admin panel, I ran:

npx create-admiral-app@latest

During setup, I picked:
“Install the template without backend setting”,
and for the project name, I enteredadmin.

That gave me a new directory: admiral-laravel-init/admin. I jumped into it and installed dependencies:

cd admin && npm i

Then I updated the .env file to point to the Laravel backend:

VITE_API_URL=http://localhost:8000/admin

Now I built and started the Admiral frontend:

npm run build && npm run dev

Once the dev server was up, I saw this in the terminal:

Local: http://localhost:3000/

Opening that URL showed the /login page. Perfect.

Step 3: Setting Up Authentication

With both Admiral and Laravel live, it was time to wire up authentication using Laravel Sanctum and Admiral’s AuthProvider interface.

Install Sanctum

First, I installed Laravel Sanctum:

php artisan install:api

Then I opened config/auth.php and registered a new admin guard:

'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'sanctum',
'provider' => 'users',
],
],

Next, I added the HasApiTokens trait to the User model:

class User extends Authenticatable
{
use HasFactory, Notifiable, HasApiTokens;
}

AuthController.php

Now it was time to create the actual AuthController:

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,
];
}
}

Supporting Files

LoginRequest.php

 ['required', 'email'],
'password' => ['required'],
];
}
public function email(): string
{
return $this->input('email');
}
public function password(): string
{
return $this->input('password');
}
}

AuthUserResource.php

resource = [
'id'    => $this->resource->id,
'name'  => $this->resource->name,
'email' => $this->resource->email,
];
return parent::toArray($request);
}
}

Step 4: The Authentication Service

Here’s how I structured my backend logic: services → admin → auth.

AuthService.php

findByEmail($email);
throw_if(
!$user || !Hash::check($password, $user->password),
ValidationException::withMessages([
'password' => __('auth.failed'),
])
);
return $user;
}
public function findByEmail(string $email): User|null
{
return User::query()->where('email', $email)->first();
}
}

LimitLoginAttempts.php

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';
}
}

Step 5: Routes + Seeding

routes/admin.php

 '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']);
});
});

Then I registered it inside bootstrap/app.php:

Route::middleware('admin')
->prefix('admin')
->group(base_path('routes/admin.php'));

Add a Seed User

Update database/seeders/DatabaseSeeder.php:

use App\Models\User;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
User::factory()->create([
'name' => 'Test User',
'email' => '[email protected]',
'password' => '12345678',
]);
}
}

Then run:

php artisan db:seed
composer run dev 

Login using the seeded credentials. If you hit a CORS issue, run:

php artisan config:publish cors

Then update config/cors.php:

'paths' => ['api/*', 'sanctum/csrf-cookie', 'admin/*'],

You’re Done

At this point, I had a fully functional Laravel + Admiral stack with token-based auth, rate limiting, and frontend integration. If you made it this far, you’re ready to move on to CRUDs, tables, dashboards, and everything else.

That’s for the next article.

Questions? Thoughts? I’m all ears — ping me on GitHub or drop an issue on Admiral.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button