Agrofin

Audit log

On this page 6

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

json
{
  "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