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
64 lines
1.8 KiB
TypeScript
64 lines
1.8 KiB
TypeScript
import { createServerClient } from '@supabase/ssr';
|
|
import { NextResponse, type NextRequest } from 'next/server';
|
|
|
|
export async function middleware(request: NextRequest) {
|
|
let response = NextResponse.next({ request });
|
|
|
|
const supabase = createServerClient(
|
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
{
|
|
cookies: {
|
|
getAll() {
|
|
return request.cookies.getAll();
|
|
},
|
|
setAll(toSet) {
|
|
toSet.forEach(({ name, value }) => request.cookies.set(name, value));
|
|
response = NextResponse.next({ request });
|
|
toSet.forEach(({ name, value, options }) =>
|
|
response.cookies.set(name, value, options),
|
|
);
|
|
},
|
|
},
|
|
},
|
|
);
|
|
|
|
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).*)',
|
|
'/admin/:path*',
|
|
'/api/admin/:path*',
|
|
],
|
|
};
|