import { type NextRequest, NextResponse } from 'next/server'; import { requireAdminApi } from '@/lib/auth/admin-guard'; import { getTunnelsList } from '@/lib/admin/list'; import { logAdminAction } from '@/lib/auth/audit'; import { toCsv, EXPORT_MAX_ROWS } from '@/lib/admin/csv'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; export async function GET(req: NextRequest) { const auth = await requireAdminApi(); if (!auth.ok) return auth.response; const url = new URL(req.url); const search = url.searchParams.get('search') ?? ''; const status = url.searchParams.get('status'); const sort = url.searchParams.get('sort'); const order = url.searchParams.get('order'); try { // Respect search/status/sort but pull the full matching set (capped). // SECURITY: the tunnel token is NEVER selected or exported. const { tunnels } = await getTunnelsList({ page: 1, perPage: EXPORT_MAX_ROWS, search, status, sort, order, }); const header = [ 'user_id', 'owner_email', 'subdomain', 'is_active', 'bytes_used', 'quota_bytes', 'usage_pct', 'last_seen_at', 'created_at', ]; const rows = tunnels.map((t) => [ t.user_id, t.owner_email ?? '', t.subdomain, t.is_active ? 'true' : 'false', t.bytes_used, t.quota_bytes, t.usage_pct.toFixed(1), t.last_seen_at ?? '', t.created_at, ]); const csv = toCsv(header, rows); await logAdminAction(auth.user, { action: 'tunnel.export', target_type: 'tunnel', details: { count: rows.length, capped: rows.length >= EXPORT_MAX_ROWS }, }); return new NextResponse(csv, { status: 200, headers: { 'Content-Type': 'text/csv; charset=utf-8', 'Content-Disposition': 'attachment; filename="tunnels.csv"', 'Cache-Control': 'no-store', Pragma: 'no-cache', }, }); } catch (e) { console.error('admin tunnels export failed', e); return NextResponse.json( { error: 'internal error' }, { status: 500, headers: { 'Cache-Control': 'no-store' } }, ); } }