126 lines
4.4 KiB
TypeScript
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`,
|
|
},
|
|
}
|
|
)
|
|
}
|