---
phase: "06.2"
plan: "04"
subsystem: "admin-audit-log"
tags: [audit-log, handle-enrichment, csv-export, daily-exports, admin, security]
dependency_graph:
  requires: ["06.2-02", "06.2-03"]
  provides: ["ADMIN-06 complete", "handle-enriched audit log", "fixed CSV export", "daily export UI"]
  affects: ["backend/api/audit.py", "frontend/src/components/admin/AuditLogTab.vue", "frontend/src/api/client.js"]
tech_stack:
  added: []
  patterns:
    - "SQLAlchemy aliased double-JOIN for handle enrichment"
    - "Handle-to-UUID resolution with empty-result fallback for unknown handles"
    - "asyncio.to_thread wrapping synchronous MinIO SDK calls"
    - "fetch+Blob URL pattern for authenticated CSV download"
    - "Date path parameter regex validation before MinIO key construction"
key_files:
  created: []
  modified:
    - backend/api/audit.py
    - backend/tests/test_audit.py
    - frontend/src/api/client.js
    - frontend/src/components/admin/AuditLogTab.vue
decisions:
  - "Module-level import of get_storage_backend and MinIOBackend in audit.py to enable testable patch targets"
  - "Separate count query (no JOIN) from data query (with JOIN) to avoid COUNT subquery ambiguity on multi-column selects (Pitfall 4)"
  - "export_audit_log uses same _audit_to_dict_with_handles() as list_audit_log to prevent UUID-only CSV export regression (Pitfall 7)"
  - "Updated test_audit_log_export_csv expected CSV header to include user_handle and actor_handle columns"
metrics:
  duration: "~25 minutes"
  completed_date: "2026-05-31"
  tasks_completed: 2
  files_modified: 4
---

# Phase 06.2 Plan 04: ADMIN-06 audit enrichment + CSV + daily exports Summary

**One-liner:** Handle-enriched audit log (aliased double-JOIN), user_handle filter with handle→UUID resolution, fixed CSV export via fetch+Blob, and new daily-export listing + streaming download endpoints with MinIO integration.

## What Was Built

### Backend (backend/api/audit.py)

**New helper functions:**
- `_audit_to_dict_with_handles(entry, user_handle, actor_handle)` — extends the existing `_audit_to_dict()` to include `user_handle` and `actor_handle` fields. Used by both the JSON viewer and CSV export (Pitfall 7 compliance).
- `_build_filtered_query_with_handles(start, end, user_uuid, event_type)` — builds a multi-column select joining `User` twice (as `UserSubject` and `UserActor` via `aliased()`) to resolve handles. Returns `(AuditLog, user_handle, actor_handle)` row tuples.

**Updated endpoints:**
- `GET /api/admin/audit-log` — `user_id: Optional[uuid.UUID]` replaced with `user_handle: Optional[str]`. Handle resolved to UUID via preliminary SELECT; unknown handles return empty results (not 422). Data query uses enriched JOIN; count query uses plain query to avoid subquery ambiguity (Pitfall 4).
- `GET /api/admin/audit-log/export` — same user_handle change; uses `_audit_to_dict_with_handles()` so CSV includes `user_handle` and `actor_handle` columns.

**New endpoints (registered before existing ones to ensure route priority):**
- `GET /api/admin/audit-log/daily-exports` — lists MinIO `audit-logs` bucket via `asyncio.to_thread(_list)`. Returns `{items: [{date, key}]}` sorted descending by date. Returns `{items: []}` if backend is not MinIOBackend.
- `GET /api/admin/audit-log/daily-exports/{date}` — validates date against `re.fullmatch(r"\d{4}-\d{2}-\d{2}", date)` before constructing `f"audit-logs/{date}.csv"` key (T-06.2-04-01 path traversal prevention). Streams CSV via `asyncio.to_thread(_get)`. Returns 404 on invalid date or missing file.

Both new endpoints use `Depends(get_current_admin)` (T-06.2-04-02).

### Backend Tests (backend/tests/test_audit.py)

Five xfail stubs promoted to full integration tests:
1. `test_audit_log_includes_user_handle` — seeds entry, asserts `user_handle` and `actor_handle` keys present, handle matches admin_user fixture
2. `test_audit_log_filter_by_handle` — seeds two users, asserts filtering by handle returns only matching entries
3. `test_audit_log_filter_unknown_handle` — asserts 200 + `items==[]` + `total==0` for unknown handle
4. `test_daily_exports_list` — mocks `get_storage_backend` with mock MinIO client, asserts sorted `items` returned
5. `test_daily_export_download` — mocks `get_object`, asserts `text/csv` Content-Type, `2026-05-30` in Content-Disposition, 404 for invalid date

Also updated `test_audit_log_export_csv` expected CSV header to include `user_handle,actor_handle` columns — regression caused by enriched export; correct per Pitfall 7.

**Final test counts:** 10 tests pass (4 pre-existing + 6 updated/promoted), full suite 337 passed / 1 pre-existing failure (test_extract_docx, ModuleNotFoundError — unrelated).

### Frontend (frontend/src/api/client.js)

Three new exported functions:
- `adminExportAuditLogCsv(params)` — raw `fetch()` with Authorization Bearer header, `res.text()` (not `res.json()`), Blob + `<a>` click download pattern (D-13, T-06.2-04-03)
- `adminListDailyExports()` — raw `fetch()` + `res.json()` for the JSON-returning listing endpoint
- `adminDownloadDailyExport(date)` — raw `fetch()` with Bearer header, Blob download as `audit-{date}.csv`

Updated `adminListAuditLog()` — parameter renamed from `user_id` to `user_handle` to match backend API change.

### Frontend (frontend/src/components/admin/AuditLogTab.vue)

- Label "User" → "User handle"; `filters.user_id` → `filters.user_handle`; `fetchLog()` passes `user_handle` param
- `exportCsv()` replaced with async function calling `api.adminExportAuditLogCsv()`; loading state `exportingCsv` ref; error display
- New "Daily exports" section below pagination: loading/empty/populated states, date `<select>` dropdown, Download button with spinner, error display
- All reactive state initialized in `<script setup>`; `loadDailyExports()` called in `onMounted()` alongside `fetchLog()`

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] Moved get_storage_backend import to module level for testability**
- **Found during:** Task 1 - daily exports tests
- **Issue:** The plan specified lazy imports inside handler bodies (`from storage import get_storage_backend`). When tests used `patch("api.audit.get_storage_backend", ...)`, the attribute did not exist on the module (the import had not yet executed), causing `AttributeError`.
- **Fix:** Moved `from storage import get_storage_backend` and `from storage.minio_backend import MinIOBackend` to module-level imports. This is consistent with how other modules import these — the lazy import pattern was only needed for the cloud backend classes (to avoid circular imports) not for the top-level factory.
- **Files modified:** backend/api/audit.py
- **Commit:** 839bfe0

**2. [Rule 1 - Bug] Updated test_audit_log_export_csv header assertion**
- **Found during:** Task 1 - running full test suite after GREEN phase
- **Issue:** The existing CSV export test asserted the old header line (without `user_handle,actor_handle`). After enriching the export endpoint per Pitfall 7, the test failed with a header mismatch.
- **Fix:** Updated `expected_header` in `test_audit_log_export_csv` to include `user_handle,actor_handle` columns. This is the correct behavior — the test was correct for the old API, and the new assertion is correct for the enriched API.
- **Files modified:** backend/tests/test_audit.py
- **Commit:** 839bfe0

## Security Compliance

All threat model mitigations implemented and verified:
- **T-06.2-04-01** (date path traversal): `re.fullmatch(r"\d{4}-\d{2}-\d{2}", date)` gates key construction
- **T-06.2-04-02** (unauthenticated access): both new endpoints use `Depends(get_current_admin)`
- **T-06.2-04-03** (CSV token bypass via window.location.href): replaced with `fetch()+Blob` pattern carrying Bearer header
- **T-06.2-04-05** (event loop blocking): `asyncio.to_thread()` wraps all synchronous MinIO SDK calls

## Known Stubs

None — all functionality is fully wired.

## Self-Check: PASSED

Files verified present:
- backend/api/audit.py — contains `_audit_to_dict_with_handles`, `_build_filtered_query_with_handles`, `/audit-log/daily-exports`, `/audit-log/daily-exports/{date}`, `re.fullmatch`
- backend/tests/test_audit.py — all 10 tests pass
- frontend/src/api/client.js — contains `adminExportAuditLogCsv`, `adminListDailyExports`, `adminDownloadDailyExport`
- frontend/src/components/admin/AuditLogTab.vue — contains "Daily exports", "User handle", no `window.location.href`

Commits verified:
- d7cfc5c — test(06.2-04): add failing tests for handle enrichment, user_handle filter, daily exports
- 839bfe0 — feat(06.2-04): backend — handle enrichment, user_handle filter, two daily-export endpoints
- 0647e6e — feat(06.2-04): frontend — user_handle filter, fetch+Blob export, daily-export section
