fb4880a1d9
Adds an authenticated admin surface gated by auth.users.app_metadata.role==='admin'. - lib/auth/admin-guard.ts: requireAdmin() (pages) + requireAdminApi() (routes) - middleware.ts: defense-in-depth /admin and /api/admin guarding - API: users (list/detail/role/ban/delete), tunnels (list + active/quota/reset/reassign/regenerate-token/delete), metrics, audit log, reserved subdomains - Self-lockout prevention (no self demote/ban/delete) - Best-effort Redis kill-switch via dependency-free net-socket client (REDIS_URL) - admin_audit_log + reserved_subdomains migration (RLS on, service-role only) - Admin UI (overview, users, tunnels, reserved, audit) + conditional nav link
81 lines
2.0 KiB
TypeScript
81 lines
2.0 KiB
TypeScript
import { getSupabaseAdmin } from '@/lib/supabase/admin';
|
|
|
|
export type AdminMetrics = {
|
|
totalUsers: number;
|
|
totalTunnels: number;
|
|
activeTunnels: number;
|
|
inactiveTunnels: number;
|
|
overQuota: number;
|
|
bytesUsedTotal: number;
|
|
quotaTotal: number;
|
|
signups7d: number;
|
|
signups30d: number;
|
|
recentlyActive: number;
|
|
};
|
|
|
|
type TunnelAgg = {
|
|
is_active: boolean;
|
|
bytes_used: number;
|
|
quota_bytes: number;
|
|
last_seen_at: string | null;
|
|
};
|
|
|
|
export async function computeMetrics(): Promise<AdminMetrics> {
|
|
const admin = getSupabaseAdmin();
|
|
|
|
const { data: tunnelsData } = await admin
|
|
.from('tunnels')
|
|
.select('is_active, bytes_used, quota_bytes, last_seen_at');
|
|
const tunnels = (tunnelsData ?? []) as TunnelAgg[];
|
|
|
|
const now = Date.now();
|
|
const day = 24 * 60 * 60 * 1000;
|
|
let activeTunnels = 0;
|
|
let inactiveTunnels = 0;
|
|
let overQuota = 0;
|
|
let bytesUsedTotal = 0;
|
|
let quotaTotal = 0;
|
|
let recentlyActive = 0;
|
|
for (const t of tunnels) {
|
|
if (t.is_active) activeTunnels++;
|
|
else inactiveTunnels++;
|
|
if (t.quota_bytes > 0 && t.bytes_used >= t.quota_bytes) overQuota++;
|
|
bytesUsedTotal += Number(t.bytes_used) || 0;
|
|
quotaTotal += Number(t.quota_bytes) || 0;
|
|
if (t.last_seen_at && now - new Date(t.last_seen_at).getTime() <= day) {
|
|
recentlyActive++;
|
|
}
|
|
}
|
|
|
|
let totalUsers = 0;
|
|
let signups7d = 0;
|
|
let signups30d = 0;
|
|
const perPage = 1000;
|
|
for (let page = 1; page <= 50; page++) {
|
|
const { data, error } = await admin.auth.admin.listUsers({ page, perPage });
|
|
if (error) break;
|
|
const users = data.users;
|
|
if (users.length === 0) break;
|
|
totalUsers += users.length;
|
|
for (const u of users) {
|
|
const created = new Date(u.created_at).getTime();
|
|
if (now - created <= 7 * day) signups7d++;
|
|
if (now - created <= 30 * day) signups30d++;
|
|
}
|
|
if (users.length < perPage) break;
|
|
}
|
|
|
|
return {
|
|
totalUsers,
|
|
totalTunnels: tunnels.length,
|
|
activeTunnels,
|
|
inactiveTunnels,
|
|
overQuota,
|
|
bytesUsedTotal,
|
|
quotaTotal,
|
|
signups7d,
|
|
signups30d,
|
|
recentlyActive,
|
|
};
|
|
}
|