---
phase: 04-folders-sharing-quotas-document-ux
plan: "06"
subsystem: admin-audit
tags: [audit-log, admin-api, celery, csv-export, minio, security]
dependency_graph:
  requires: ["04-03", "04-04"]
  provides: ["ADMIN-06", "D-17"]
  affects: ["backend/api/audit.py", "backend/tasks/audit_tasks.py", "backend/celery_app.py", "backend/main.py"]
tech_stack:
  added: []
  patterns:
    - "Admin-only audit log viewer with paginated, filtered SQLAlchemy query"
    - "Streaming CSV export via FastAPI StreamingResponse + csv.DictWriter"
    - "Celery beat crontab schedule at midnight UTC for daily MinIO export"
    - "Deferred imports inside async task body to prevent circular imports"
    - "_audit_to_dict() safe whitelist serializer pattern (mirrors _user_to_dict)"
key_files:
  created:
    - backend/api/audit.py
    - backend/tasks/audit_tasks.py
  modified:
    - backend/celery_app.py
    - backend/main.py
decisions:
  - "CSV export reuses _audit_to_dict() whitelist helper — single source of truth for safe field set"
  - "audit_tasks.* routed to documents queue — reuses existing documents worker (no new queue needed)"
  - "crontab alias uses _crontab (underscore prefix) consistent with existing _timedelta alias"
metrics:
  duration_seconds: 262
  completed_date: "2026-05-25"
  tasks_completed: 2
  files_created: 2
  files_modified: 2
---

# Phase 4 Plan 06: Admin Audit Log API + Celery Daily Export Summary

**One-liner:** Admin-only paginated/filtered audit log viewer with CSV streaming export (ADMIN-06) and midnight-UTC Celery beat task uploading daily CSVs to MinIO audit-logs bucket (D-17).

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Admin audit log viewer + CSV export | 364447d | backend/api/audit.py, backend/main.py |
| 2 | Celery daily export task + beat schedule | f89f787 | backend/tasks/audit_tasks.py, backend/celery_app.py |

## What Was Built

### Task 1: backend/api/audit.py

Two admin-only endpoints protected by `Depends(get_current_admin)`:

- `GET /api/admin/audit-log` — paginated (page/per_page), filtered (start, end, user_id, event_type). Returns `{items, total, page, per_page}`. Runs a separate COUNT query for total using the same filters.
- `GET /api/admin/audit-log/export` — same filter params, no pagination; streams CSV with `Content-Disposition: attachment; filename=audit-export.csv`.

The `_audit_to_dict()` helper is the single source of truth for the safe field set: `id, event_type, user_id, actor_id, resource_id, ip_address, metadata_, created_at`. The dict literal contains no `filename`, `extracted_text`, `password_hash`, or `credentials_enc` keys. Both the JSON and CSV paths use this same helper.

### Task 2: backend/tasks/audit_tasks.py + celery_app.py

- `audit_log_daily_export` Celery task: sync entry point → `asyncio.run(_run_daily_export())`.
- `_run_daily_export()`: queries yesterday's `AuditLog` rows (UTC midnight to midnight), writes CSV via `csv.DictWriter`, uploads to MinIO via `put_object_raw(bucket="audit-logs", key="audit-logs/YYYY-MM-DD.csv", ...)`. Wraps everything in try/except — returns `{"exported": 0, "error": str(e)}` on failure.
- All imports deferred inside `_run_daily_export()` body (same circular-import-prevention pattern as `document_tasks._run`).
- `celery_app.py`: `_crontab` aliased import, beat entry `"audit-log-daily-export"` at `_crontab(hour=0, minute=0)`, task route `"tasks.audit_tasks.*": {"queue": "documents"}`.

## Deviations from Plan

None — plan executed exactly as written.

## Security Invariants Verified

| Threat ID | Check | Result |
|-----------|-------|--------|
| T-04-06-01 | `Depends(get_current_admin)` on both endpoints (grep: 2 occurrences at lines 94, 129) | PASS |
| T-04-06-02 | `_audit_to_dict()` dict literal contains no forbidden keys (grep: filename/extracted_text only in comments) | PASS |
| T-04-06-03 | CSV export uses same `_audit_to_dict()` helper as JSON viewer | PASS |
| T-04-06-04 | `put_object_raw` uses `bucket="audit-logs"` (not documents bucket) | PASS |

## Test Results

```
tests/test_audit.py: 4 xfailed (stub tests from Wave 0 — plan 04-06 implements the API,
  detailed integration tests will be written in the full TDD pass)
Full suite: 1 failed (test_extractor.py::test_extract_docx — pre-existing missing module,
  out of scope), 130 passed, 7 skipped, 35 xfailed
```

Pre-existing failures (not caused by this plan):
- `test_extractor.py::test_extract_docx` — missing python-docx module in local env
- `test_documents.py::test_content_stream_200` — intentional TDD RED from plan 04-05 (commit 8e6cb6e)

## Known Stubs

None — both endpoints are fully implemented and wired.

## Threat Flags

None — no new network endpoints or trust boundaries beyond those documented in the plan's threat model.

## Self-Check: PASSED

- [x] `backend/api/audit.py` exists: FOUND
- [x] `backend/tasks/audit_tasks.py` exists: FOUND
- [x] Task 1 commit 364447d: FOUND
- [x] Task 2 commit f89f787: FOUND
- [x] `python3 -c "from api.audit import router"` exits 0: PASS
- [x] `python3 -c "from tasks.audit_tasks import audit_log_daily_export"` exits 0: PASS
- [x] `beat_schedule` contains `audit-log-daily-export`: PASS
- [x] `task_routes` contains `tasks.audit_tasks.*`: PASS
