Every login attempt that reaches a credential check — successful or not, and
through either login endpoint — is recorded to an append-only audit trail
(spatie/laravel-activitylog, table activity_log). Admins holding the
view audit-log permission can read it through a single paginated, filterable
endpoint. There is no write/delete API for this data — the trail is read-only
by design.
What gets recorded
Both endpoints write one activity-log entry per attempt, under
log_name = auth. The Bearer-token endpoint is covered on purpose:
config/sanctum.php pins 'guard' => ['web'], so a token issued by
POST /api/v1/login is accepted on every /api/v1/admin/* route — logging
only the cookie login would leave a way to drive the whole admin API without
a single row in the trail.
| Event | When | description |
causer |
properties |
|---|---|---|---|---|
login |
POST /api/v1/admin/login: credentials valid, session established |
Admin logged in |
The logging-in admin | ip, user_agent |
login |
POST /api/v1/login: credentials valid, token issued |
Logged in with an API token |
The logging-in user | ip, user_agent |
login_failed |
POST /api/v1/admin/login: wrong password, unknown email, inactive account, or no session could be started (see Admin panel authentication) |
Admin login failed |
— (unauthenticated) | email (the attempted address), ip, user_agent |
login_failed |
POST /api/v1/login: wrong password, unknown email or inactive account |
API token login failed |
— (unauthenticated) | email (the attempted address), ip, user_agent |
One case writes nothing: a request the email+IP lockout turns away with 429 never reaches a credential check, so it produces no row. The five failures that tripped that lockout are already in the trail.
The attempted password is never recorded — not in properties, not in
the description, not anywhere in the row. Only the email address (for a
failed attempt) and connection metadata are stored.
GET /api/v1/admin/audit-log
| Auth | Session or Bearer token (auth:sanctum accepts either) |
| Permission | view audit-log — 403 without it |
| Sort | sort=-id by default (newest first) |
Filters
| Filter | Example | Notes |
|---|---|---|
filter[log_name] |
?filter[log_name]=auth |
Exact match |
filter[event] |
?filter[event]=login_failed |
Exact match |
filter[causer_id] |
?filter[causer_id]=42 |
Exact match |
filter[subject_type] |
?filter[subject_type]=App\Models\User |
Exact match |
Sorts
created_at, id — e.g. ?sort=created_at or ?sort=-id.
Pagination follows the standard contract in
Lists, filtering & pagination: per_page, meta.pagination.
Response shape
{
"data": [
{
"id": 17,
"log_name": "auth",
"event": "login_failed",
"description": "Admin login failed",
"subject_type": null,
"subject_id": null,
"causer": null,
"properties": {
"email": "admin@agrofin.uz",
"ip": "127.0.0.1",
"user_agent": "Mozilla/5.0 ..."
},
"created_at": "2026-09-09T10:15:00+00:00"
}
],
"meta": {
"pagination": { "total": 1, "per_page": 25, "current_page": 1, "last_page": 1 }
}
}
causer is null for a login_failed entry (no user was authenticated yet)
and { "id": ..., "name": ... } for a login entry.
Errors
| HTTP | When |
|---|---|
| 401 | No active session |
| 403 | Authenticated but missing view audit-log |