Task #95: Let admins export the audit log as CSV
What was done - Added a new admin-only endpoint GET /api/admin/audit-logs/export that streams the filtered audit log as a CSV download. Honors the same `action`, `from`, and `to` query parameters as the list endpoint, validated through a shared parseFilters/buildWhere helper extracted from the existing handler. - Columns: action, actor_username, target_type, target_id, created_at, metadata (compact JSON). Rows ordered newest-first and capped at 50,000 to bound response size. UTF-8 BOM prepended so spreadsheet apps (incl. Excel) detect encoding correctly for Arabic content stored in metadata. - Response sets Content-Type: text/csv; charset=utf-8, Content-Disposition: attachment; filename="audit-log-YYYY-MM-DD.csv", Cache-Control: no-store, and is written via res.write streaming. - Updated lib/api-spec/openapi.yaml with operationId exportAuditLogsCsv and ran codegen to regenerate the React client + Zod schemas. - Frontend (artifacts/tx-os/src/pages/admin.tsx → AuditLogPanel): added an "Export CSV" button next to the existing filter actions. Clicking it fetches the export URL with current filters using same-origin cookies, triggers a browser download, and surfaces a localized error if the request fails. The button is disabled while filters are invalid or while a download is in flight, and a spinner replaces the icon during export. - Added bilingual translation keys admin.audit.export.button / admin.audit.export.failed in en.json and ar.json. Security hardening (post code-review) - csvEscape now prefixes a single quote when a cell value begins with one of =, +, -, @, tab, or CR, mitigating CSV/spreadsheet formula injection (Excel, LibreOffice, Google Sheets evaluate such cells as formulas). Verified end-to-end by inserting a synthetic audit row whose action started with "=" and confirming the export wrote it as "'=...". Verification - Typecheck of tx-os passes; api-server has only pre-existing executive-meetings errors unrelated to this change. - Manual curl smoke test: 200 with correct headers, BOM present, action and date filters honored, invalid date returns 400. - E2e UI test (Playwright via testing skill): logged in as admin, opened the Audit Log section, downloaded the unfiltered CSV (correct filename and header row), then re-exported with action=app.delete and confirmed only matching rows came back. - Targeted security check confirmed CSV injection mitigation (see above). Notes / drift - None for the spec. The generated `exportAuditLogsCsv()` helper in lib/api-client-react is typed Promise<Blob> but, because customFetch auto-infers text/csv as text, would currently return text if anyone called it directly. The Audit Log UI uses a raw fetch with credentials: "include" to download the blob, so this does not affect the feature. Updating the generated client's responseType handling is project-wide config and is intentionally out of scope here. Follow-ups proposed - #123 Add automated tests for the audit log CSV export (test_gaps) - #124 Let admins pick which columns to include when exporting the audit log (next_steps) Replit-Task-Id: 88a50100-caa7-4b37-b9da-cfdc42bee119
This commit is contained in:
@@ -1854,6 +1854,52 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/admin/audit-logs/export:
|
||||
get:
|
||||
operationId: exportAuditLogsCsv
|
||||
tags: [audit]
|
||||
summary: Export filtered audit log as CSV (admin)
|
||||
description: |
|
||||
Streams the filtered audit log as a CSV download.
|
||||
Honors the same `action`, `from`, and `to` filters as the list endpoint.
|
||||
Columns: action, actor_username, target_type, target_id, created_at, metadata.
|
||||
Capped at 50,000 rows per export to bound response size.
|
||||
parameters:
|
||||
- in: query
|
||||
name: action
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: Exact action name to filter by (e.g. "group.force_delete").
|
||||
- in: query
|
||||
name: from
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Start date (inclusive, YYYY-MM-DD UTC).
|
||||
- in: query
|
||||
name: to
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: End date (inclusive, YYYY-MM-DD UTC).
|
||||
responses:
|
||||
"200":
|
||||
description: CSV file download
|
||||
content:
|
||||
text/csv:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"400":
|
||||
description: Invalid filter parameters
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
HealthStatus:
|
||||
|
||||
Reference in New Issue
Block a user