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
This commit is contained in:
+33
-2
@@ -23,10 +23,41 @@ export async function middleware(request: NextRequest) {
|
||||
},
|
||||
);
|
||||
|
||||
await supabase.auth.getUser();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
// Defense-in-depth: guard the admin surface here in addition to the
|
||||
// per-route requireAdmin()/requireAdminApi() checks.
|
||||
const path = request.nextUrl.pathname;
|
||||
if (path.startsWith('/admin') || path.startsWith('/api/admin')) {
|
||||
if (!user) {
|
||||
if (path.startsWith('/api/admin')) {
|
||||
return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
|
||||
}
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = '/login';
|
||||
url.search = '';
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
if (user.app_metadata?.role !== 'admin') {
|
||||
if (path.startsWith('/api/admin')) {
|
||||
return NextResponse.json({ error: 'forbidden' }, { status: 403 });
|
||||
}
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = '/dashboard';
|
||||
url.search = '';
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!_next/static|_next/image|favicon.ico|api/tunnel/claim).*)'],
|
||||
matcher: [
|
||||
'/((?!_next/static|_next/image|favicon.ico|api/tunnel/claim).*)',
|
||||
'/admin/:path*',
|
||||
'/api/admin/:path*',
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user