mission-control/src/app/api/export/route.ts

126 lines
4.4 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { requireRole } from '@/lib/auth'
import { getDatabase, logAuditEvent } from '@/lib/db'
import { heavyLimiter } from '@/lib/rate-limit'
/**
* GET /api/export?type=audit|tasks|activities|pipelines&format=csv|json&since=UNIX&until=UNIX
* Admin-only data export endpoint.
*/
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'admin')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = heavyLimiter(request)
if (rateCheck) return rateCheck
const { searchParams } = new URL(request.url)
const type = searchParams.get('type')
const format = searchParams.get('format') || 'csv'
const since = searchParams.get('since')
const until = searchParams.get('until')
if (!type || !['audit', 'tasks', 'activities', 'pipelines'].includes(type)) {
return NextResponse.json(
{ error: 'type required: audit, tasks, activities, pipelines' },
{ status: 400 }
)
}
const db = getDatabase()
const conditions: string[] = []
const params: any[] = []
if (since) {
conditions.push('created_at >= ?')
params.push(parseInt(since))
}
if (until) {
conditions.push('created_at <= ?')
params.push(parseInt(until))
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
const requestedLimit = parseInt(searchParams.get('limit') || '10000')
const maxLimit = 50000
const limit = Math.min(requestedLimit, maxLimit)
let rows: any[] = []
let headers: string[] = []
let filename = ''
switch (type) {
case 'audit': {
rows = db.prepare(`SELECT * FROM audit_log ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit)
headers = ['id', 'action', 'actor', 'actor_id', 'target_type', 'target_id', 'detail', 'ip_address', 'user_agent', 'created_at']
filename = 'audit-log'
break
}
case 'tasks': {
rows = db.prepare(`SELECT * FROM tasks ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit)
headers = ['id', 'title', 'description', 'status', 'priority', 'assigned_to', 'created_by', 'created_at', 'updated_at', 'due_date', 'estimated_hours', 'actual_hours', 'tags']
filename = 'tasks'
break
}
case 'activities': {
rows = db.prepare(`SELECT * FROM activities ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit)
headers = ['id', 'type', 'entity_type', 'entity_id', 'actor', 'description', 'data', 'created_at']
filename = 'activities'
break
}
case 'pipelines': {
rows = db.prepare(`SELECT pr.*, wp.name as pipeline_name FROM pipeline_runs pr LEFT JOIN workflow_pipelines wp ON pr.pipeline_id = wp.id ${where ? where.replace('created_at', 'pr.created_at') : ''} ORDER BY pr.created_at DESC LIMIT ?`).all(...params, limit)
headers = ['id', 'pipeline_id', 'pipeline_name', 'status', 'current_step', 'steps_snapshot', 'started_at', 'completed_at', 'triggered_by', 'created_at']
filename = 'pipeline-runs'
break
}
}
// Log the export
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
logAuditEvent({
action: 'data_export',
actor: auth.user.username,
actor_id: auth.user.id,
detail: { type, format, row_count: rows.length },
ip_address: ipAddress,
})
const dateStr = new Date().toISOString().split('T')[0]
if (format === 'csv') {
const csvRows = [headers.join(',')]
for (const row of rows) {
const values = headers.map(h => {
const val = row[h]
if (val == null) return ''
const str = String(val)
// Escape CSV: wrap in quotes if contains comma, newline, or quote
if (str.includes(',') || str.includes('\n') || str.includes('"')) {
return `"${str.replace(/"/g, '""')}"`
}
return str
})
csvRows.push(values.join(','))
}
return new NextResponse(csvRows.join('\n'), {
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': `attachment; filename=${filename}-${dateStr}.csv`,
},
})
}
// JSON format
return NextResponse.json(
{ type, exported_at: new Date().toISOString(), count: rows.length, data: rows },
{
headers: {
'Content-Disposition': `attachment; filename=${filename}-${dateStr}.json`,
},
}
)
}