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 = { user_id: string; subdomain: string; is_active: boolean; bytes_used: number; quota_bytes: number; }; 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 admin = getSupabaseAdmin(); const { data, error } = await admin.auth.admin.listUsers({ page, perPage }); if (error) { return NextResponse.json({ error: error.message }, { status: 500 }); } let users = data.users; const total = (data as unknown as { total?: number }).total ?? users.length; if (search) { users = users.filter((u) => (u.email ?? '').toLowerCase().includes(search), ); } // Join tunnel rows for this page's users in a single query. const ids = users.map((u) => u.id); const tunnelMap = new Map(); if (ids.length > 0) { const { data: tunnels } = await admin .from('tunnels') .select('user_id, subdomain, is_active, bytes_used, quota_bytes') .in('user_id', ids); for (const t of (tunnels ?? []) as TunnelRow[]) { tunnelMap.set(t.user_id, t); } } const result = users.map((u) => { const t = tunnelMap.get(u.id) ?? null; return { id: u.id, email: u.email ?? null, role: (u.app_metadata?.role as string | undefined) ?? 'user', banned_until: (u as unknown as { banned_until?: string | null }) .banned_until ?? null, email_confirmed_at: u.email_confirmed_at ?? null, created_at: u.created_at, last_sign_in_at: u.last_sign_in_at ?? null, tunnel: t ? { subdomain: t.subdomain, is_active: t.is_active, bytes_used: t.bytes_used, quota_bytes: t.quota_bytes, } : null, }; }); return NextResponse.json({ users: result, total, page, perPage }); }