import { NextResponse, type NextRequest } from 'next/server'; import { requireAdminApi } from '@/lib/auth/admin-guard'; import { getSupabaseAdmin } from '@/lib/supabase/admin'; import { parsePageParam, parsePerPageParam } from '@/lib/admin/validators'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; type TunnelRow = { id: string; user_id: string; subdomain: string; is_active: boolean; bytes_used: number; quota_bytes: number; last_seen_at: string | null; created_at: string; }; export async function GET(req: NextRequest) { const auth = await requireAdminApi(); if (!auth.ok) return auth.response; const url = new URL(req.url); const page = parsePageParam(url.searchParams.get('page'), 1); const perPage = parsePerPageParam(url.searchParams.get('perPage'), 25, 100); const search = (url.searchParams.get('search') ?? '').trim().toLowerCase(); const status = url.searchParams.get('status'); // active|inactive|over_quota const admin = getSupabaseAdmin(); const cols = 'id, user_id, subdomain, is_active, bytes_used, quota_bytes, last_seen_at, created_at'; let rows: TunnelRow[]; let total: number; const from = (page - 1) * perPage; const to = from + perPage - 1; if (status === 'over_quota') { // Column-to-column comparison is not expressible via PostgREST filters, // so fetch matching rows and paginate in memory. let q = admin.from('tunnels').select(cols); if (search) q = q.ilike('subdomain', `%${search}%`); const { data, error } = await q.order('created_at', { ascending: false }); if (error) { return NextResponse.json({ error: error.message }, { status: 500 }); } const all = ((data ?? []) as TunnelRow[]).filter( (t) => t.quota_bytes > 0 && t.bytes_used >= t.quota_bytes, ); total = all.length; rows = all.slice(from, from + perPage); } else { let query = admin.from('tunnels').select(cols, { count: 'exact' }); if (search) query = query.ilike('subdomain', `%${search}%`); if (status === 'active') query = query.eq('is_active', true); else if (status === 'inactive') query = query.eq('is_active', false); query = query.order('created_at', { ascending: false }).range(from, to); const { data, error, count } = await query; if (error) { return NextResponse.json({ error: error.message }, { status: 500 }); } rows = (data ?? []) as TunnelRow[]; total = count ?? rows.length; } // Resolve owner emails (per-row getUserById; acceptable for current scale). const emails = await Promise.all( rows.map(async (t) => { try { const { data: u } = await admin.auth.admin.getUserById(t.user_id); return u.user?.email ?? null; } catch { return null; } }), ); const tunnels = rows.map((t, i) => ({ id: t.id, user_id: t.user_id, owner_email: emails[i], subdomain: t.subdomain, is_active: t.is_active, bytes_used: t.bytes_used, quota_bytes: t.quota_bytes, usage_pct: t.quota_bytes > 0 ? Math.min(100, (t.bytes_used / t.quota_bytes) * 100) : 0, last_seen_at: t.last_seen_at, created_at: t.created_at, })); return NextResponse.json({ tunnels, total, page, perPage }); }