The admin panel (a separate React SPA, served from its own domain) does not use the Bearer-token flow described in Getting started. It authenticates with an httpOnly session cookie via Laravel Sanctum's SPA support, so the token never touches browser JavaScript.
The three-step flow
GET /sanctum/csrf-cookie— call this once before the first state-changing request (and again after the session ends). It sets theXSRF-TOKENcookie; your HTTP client (axios, fetch) must read it and echo it back as theX-XSRF-TOKENheader on every subsequent request. Axios does this automatically whenwithCredentials: trueis set.POST /api/v1/admin/login— sendemail+password. On success the response sets the session cookie (Set-Cookie) and returns the profile below. On failure it behaves exactly like a bad-credentials response — no distinction between "wrong password" and "no such account", and an inactive account is rejected the same way.- Every request after that must be sent with
withCredentials: true(or the cookie-equivalent for your HTTP client) so the browser attaches the session cookie. NoAuthorizationheader is needed or used.
This only works from a domain listed in SANCTUM_STATEFUL_DOMAINS — any other
origin falls back to stateless (bearer-token) handling and cookie login will
silently not authenticate it.
Endpoints
| Endpoint | Auth | Notes |
|---|---|---|
POST /api/v1/admin/login |
— | email, password; sets the session cookie; throttled 10/min |
POST /api/v1/admin/logout |
Session | invalidates the session and rotates the CSRF token |
GET /api/v1/admin/me |
Session | current admin's profile and permissions |
AdminUserResource fields
GET /api/v1/admin/me and a successful POST /api/v1/admin/login both
return this shape under data, in this exact order:
| Field | Type | Notes |
|---|---|---|
id |
number | |
name |
string | |
email |
string | |
avatarUrl |
string | null | null when no avatar media is attached |
isSuperadmin |
boolean | true only for the super_admin role — the panel can use this to unlock every screen without checking individual permissions |
permissions |
string[] | flat, deduplicated list of every permission the user's roles grant (a super_admin gets the full permission list) |
roles |
string[] | role names assigned to the user |
The panel is expected to drive its own routing and UI visibility off
isSuperadmin and permissions — never off roles directly, since role-to-
permission mapping can change without a frontend release.
Error responses
| HTTP | When |
|---|---|
| 401 | login with wrong/unknown credentials or an inactive account; me/logout with no active session |
| 403 | Authenticated but the action is not permitted (enforced per-endpoint, not by these three routes) |
| 429 | More than 10 login attempts per minute from the same client, or the email+IP brute-force guard (5 failed attempts / 15 minutes) has tripped — the message carries the retry-after in seconds |
Brute-force protection
POST /api/v1/admin/login is guarded twice:
- A route-level rate limit of 10 requests/minute per client (
throttle:10,1). - A slower email+IP lockout: 5 failed attempts lock that email+IP pair out for 15 minutes, independent of the route throttle. A successful login clears the counter immediately.
Both limits return 429 with the same localized "too many attempts" message.