Files
linumiq_net-web_app/lib/admin/metrics.ts
T
Gerhard Scheikl fb4880a1d9 feat(admin): comprehensive admin interface (users, tunnels, metrics, audit, reserved subdomains)
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
2026-05-31 10:58:23 +02:00

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,
};
}