--- 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 + `` 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 `