Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions apps/web/src/app/api/audit-log/export/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server'
import { createServiceClient } from '@/lib/supabase'

/**
* GET /api/audit-log/export?certificate_id=<id>[&from=ISO&to=ISO]
*
* Public (no-auth) CSV export of audit log entries for a specific certificate.
* Intended for auditors and certificate buyers on the public verify page.
*/
export async function GET(req: NextRequest) {
const { searchParams } = req.nextUrl
const certificateId = searchParams.get('certificate_id')
if (!certificateId) {
return NextResponse.json({ error: 'certificate_id is required' }, { status: 400 })
}

const from = searchParams.get('from') ?? new Date(Date.now() - 30 * 86_400_000).toISOString()
const to = searchParams.get('to') ?? new Date().toISOString()

const db = createServiceClient()
const { data, error } = await db
.from('audit_log')
.select('id,operator_id,action,resource_id,ip_address,metadata,created_at')
.eq('resource_id', certificateId)
.gte('created_at', from)
.lte('created_at', to)
.order('created_at', { ascending: true })

if (error) return NextResponse.json({ error: error.message }, { status: 500 })

const header = 'id,operator_id,action,resource_id,ip_address,metadata,created_at\n'
const rows = (data ?? []).map(r =>
[r.id, r.operator_id, r.action, r.resource_id ?? '', r.ip_address ?? '',
JSON.stringify(r.metadata ?? {}), r.created_at]
.map(v => `"${String(v).replace(/"/g, '""')}"`)
.join(',')
).join('\n')

const filename = `audit_${certificateId}_${from.slice(0, 10)}_${to.slice(0, 10)}.csv`
return new NextResponse(header + rows, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': `attachment; filename="${filename}"`,
},
})
}
51 changes: 51 additions & 0 deletions docs/backlog/audit-trail-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Backlog: Public Verifier Audit Trail Export

**Issue:** #615
**Status:** In Progress
**Priority:** Medium

## Summary

Expose a public, no-auth CSV export endpoint so auditors and certificate buyers can download their own audit trail directly from the verify page — without needing an account or operator credentials.

## Background

`GET /api/audit-log` already exports CSV filtered by date range and operator ID, but it is intended for authenticated operators. There is no way for a third-party auditor to export the audit trail for a specific certificate they hold.

## Feature Description

Add `GET /api/audit-log/export` — a public endpoint filtered by `certificate_id` (required) plus optional `from`/`to` date range. The response is a CSV file the auditor can download locally or import into a spreadsheet.

### Endpoint

```
GET /api/audit-log/export?certificate_id=<id>[&from=ISO&to=ISO]
```

| Param | Required | Description |
|---|---|---|
| `certificate_id` | ✅ | Certificate (resource) ID to filter by |
| `from` | ❌ | ISO 8601 start date (default: 30 days ago) |
| `to` | ❌ | ISO 8601 end date (default: now) |

### Response

`200 text/csv` — rows from `audit_log` where `resource_id = certificate_id`, ordered by `created_at` ascending.
`400` — if `certificate_id` is missing.

### UI hook

Add a "Download audit trail" button on `/verify?id=<certificate_id>` that calls this endpoint with the certificate ID pre-filled.

## Acceptance Criteria

- [ ] `GET /api/audit-log/export?certificate_id=X` returns CSV without authentication
- [ ] Missing `certificate_id` returns HTTP 400
- [ ] Optional `from`/`to` params narrow the date range
- [ ] CSV filename includes the certificate ID and date range
- [ ] Verify page exposes a download button that calls the endpoint

## Out of Scope

- Pagination (export is assumed to be bounded by date range)
- Auth-gated bulk export (covered by existing `/api/audit-log`)
Loading