fix(admin): key tunnels by user_id, server-side initial list load, full-scan user search

This commit is contained in:
Gerhard Scheikl
2026-05-31 11:46:14 +02:00
parent fb4880a1d9
commit b6c4d94990
19 changed files with 1676 additions and 840 deletions
+7 -61
View File
@@ -1,19 +1,11 @@
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';
import { getUsersList } from '@/lib/admin/list';
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;
@@ -21,58 +13,12 @@ export async function GET(req: NextRequest) {
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 search = url.searchParams.get('search') ?? '';
const admin = getSupabaseAdmin();
const { data, error } = await admin.auth.admin.listUsers({ page, perPage });
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
try {
const { users, total } = await getUsersList({ page, perPage, search });
return NextResponse.json({ users, total, page, perPage });
} catch (e) {
return NextResponse.json({ error: (e as 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<string, TunnelRow>();
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 });
}