# 20 — User Activity Log

## Overview

Audit trail of admin actions. Every non-AJAX, non-default-URL request that reaches `UserPermission` middleware writes a `viso_user_activity_log` row (when toggled on).

**Status:** Live

The viewer is gated by permission group 6 ("Debug / Impersonate"). The log is currently `INSERT`-only with no rotation / archive policy in code.

---

## User Stories

| ID | As a | I want to | So that |
|----|------|-----------|---------|
| LOG-01 | Ops with perm 6 | See a paginated list of all admin actions | Audit |
| LOG-02 | Ops | See email, phone, action URI, request payload, IP, browser, last_login_from, time | Identify who did what |
| LOG-03 | Sysadmin | Toggle logging off via `.env` config | Stop noise on local |

---

## Screens & Flows

```
Admin makes a request (non-AJAX, non-default URL)
   │
   ▼
┌─────────────────────────────────────────┐
│ UserPermission middleware               │
│  → UserActivityLog::addUserLog($req)    │
│      INSERT viso_user_activity_log      │
│      (user_id, impersonated_user,       │
│       action=uri, request_payload=json, │
│       ip_address, browser)              │
└─────────────────────────────────────────┘

View log:
┌──────────────────────────────────────┐
│ /user/activity     activity/index    │
└──────────────────┬───────────────────┘
                   │ DataTable AJAX
                   ▼
┌──────────────────────────────────────┐
│ /user/activity/ajaxUsersList         │
│ OR /ajaxActivityLogList              │
│ UserActivityLog::getUserActivityLog()│
│  WHERE request_payload IS NOT NULL   │
│  ORDER BY created_at DESC            │
└──────────────────────────────────────┘
```

### Routes & Actions

| Route | Method | Handler | Description |
|-------|--------|---------|-------------|
| `/user/activity` | GET | `UserActivityController::activitylog()` | List page |
| `/user/activity/ajaxUsersList` | GET | `UserActivityController::activityList()` | DataTable JSON |
| `/ajaxActivityLogList` | GET | `UserActivityController::activityList()` | Alias of above (legacy) |

---

## Data Model

### `viso_user_activity_log`

```php
// App\Models\UserActivityLog
protected $table = 'viso_user_activity_log';

{
  id:                  int,
  user_id:             int,                // admins.id (active user)
  impersonated_user:   int|null,           // when admin acts as someone else
  action:              string,             // route URI
  request_payload:     json string|null,   // $request->all() snapshot
  ip_address:          string,
  browser:             string,             // User-Agent header
  created_at:          datetime,
  updated_at:          datetime,
  // NOTE: also reads `phone` and `last_login_from` for the display row,
  // but those aren't written by addUserLog — they may be denormalised
  // fields populated elsewhere. **Verify.**
}
```

### Display joins to `admins`

`UserActivityLog::userDetails()` relation: `hasOne(User::class, 'id', 'user_id')`. The list view shows `$row->userDetails->email`.

---

## Validations & Business Rules

| Rule | Detail |
|------|--------|
| Logging gated by config | `if (!config('app.activity_log')) return;` |
| Skips AJAX endpoints | The middleware calls `addUserLog` only AFTER checking `ajaxRequest($uri)` — so AJAX whitelisted routes are NOT logged |
| Skips default URLs | `welcome`, `login`, `logout` |
| Logs failures softly | `try/catch` around the insert; failure is logged-only |
| List filters out empty payload | `whereNotNull('request_payload')` — GET pages with empty `$request->all()` may be excluded |
| Sort | `created_at DESC` |
| Browser column | full User-Agent string |
| No rotation | All logs accumulate; ops must purge by SQL |

---

## API Endpoints

| Method | Path | Auth | Request | Response | Consumer |
|--------|------|------|---------|----------|----------|
| GET | `/user/activity` | session + permission 6 | — | HTML | Browser |
| GET | `/user/activity/ajaxUsersList` | session (AJAX whitelisted) | `draw, start, length` | DataTable JSON | List page |
| GET | `/ajaxActivityLogList` | session (AJAX whitelisted) | (same) | (same) | Legacy alias |

---

## Upstream Impact

- **`UserPermission` middleware** — the writer.
- **`admins` table** — `user_id` joins back here.
- **`config('app.activity_log')`** — toggle.

---

## Downstream Impact

- **No automated consumers** — the log is human-readable only.

---

## Impact of Changes

| If you change... | Risk to... | Level | Type |
|-----------------|------------|-------|------|
| Disabling the middleware | Activity log stops recording silently | Critical | Service |
| Renaming `viso_user_activity_log` table | Both writer and viewer break | Critical | Data |
| Renaming `request_payload` | List filter drops everything | High | Data |
| Adding huge payloads (e.g. file uploads) | Log table bloats | High | Data |
| Removing the `whereNotNull('request_payload')` filter | GET-only requests now appear; volume balloons | Medium | UI |
| Switching `userDetails()` to a different relation | Email column blank | Medium | UI |

---

## Known Issues

- **`phone` and `last_login_from` are referenced in the list view** (`$row->phone`, `$row->last_login_from`) but `addUserLog` does NOT populate them. The columns either come from a join (not declared) or are blank. **Verify the actual schema** — the model likely is missing relations or denormalised fields.
- **No rotation** — table grows forever. Past a year, list scrolling is painful.
- **Action column is the URI, not a human label** — "user/admin/45" requires context to interpret.
- **`request_payload` may contain secrets** — POST bodies are stored verbatim including passwords (e.g. new admin creation logs the plaintext password). **Compliance risk.**
- **`impersonated_user` field is written** but there's no impersonation feature in this codebase. Future-stub.
