diff --git a/apps/web/src/app/api/audit-log/export/route.ts b/apps/web/src/app/api/audit-log/export/route.ts new file mode 100644 index 0000000..ce40735 --- /dev/null +++ b/apps/web/src/app/api/audit-log/export/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createServiceClient } from '@/lib/supabase' + +/** + * GET /api/audit-log/export?certificate_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}"`, + }, + }) +} diff --git a/docs/backlog/audit-trail-export.md b/docs/backlog/audit-trail-export.md new file mode 100644 index 0000000..cbf66fa --- /dev/null +++ b/docs/backlog/audit-trail-export.md @@ -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=[&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=` 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`)