Skip to content

Instantly share code, notes, and snippets.

@swdevbali
Created October 13, 2025 03:46
Show Gist options
  • Save swdevbali/67040bbef06a91c691ea149020f40423 to your computer and use it in GitHub Desktop.
Save swdevbali/67040bbef06a91c691ea149020f40423 to your computer and use it in GitHub Desktop.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { createServerClient as createSupabaseServerClient } from '@supabase/ssr'
// Rate limiting map (in production, use Redis or similar)
const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
export async function middleware(request: NextRequest) {
// Rate limiting
const clientId = request.ip || request.headers.get('x-forwarded-for') || 'unknown'
const now = Date.now()
const windowMs = 60000 // 1 minute
const maxRequests = 100
if (rateLimitMap.has(clientId)) {
const { count, resetTime } = rateLimitMap.get(clientId)!
if (now < resetTime) {
if (count >= maxRequests) {
return new Response('Too Many Requests', { status: 429 })
}
rateLimitMap.set(clientId, { count: count + 1, resetTime })
} else {
rateLimitMap.set(clientId, { count: 1, resetTime: now + windowMs })
}
} else {
rateLimitMap.set(clientId, { count: 1, resetTime: now + windowMs })
}
// Validate request path for security
const path = request.nextUrl.pathname
if (path.includes('..') || path.includes('//') || path.includes('\\')) {
return new Response('Invalid path', { status: 400 })
}
// Validate environment variables
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!supabaseUrl || !serviceRoleKey) {
console.error('[Middleware] Missing required environment variables')
return new Response('Service unavailable', { status: 503 })
}
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
// Add security headers
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-XSS-Protection', '1; mode=block')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
// Use service role key to validate sessions
const supabase = createSupabaseServerClient(
supabaseUrl,
serviceRoleKey,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: any) {
request.cookies.set({
name,
value,
...options,
})
response = NextResponse.next({
request: {
headers: request.headers,
},
})
response.cookies.set({
name,
value,
...options,
})
},
remove(name: string, options: any) {
request.cookies.set({
name,
value: '',
...options,
})
response = NextResponse.next({
request: {
headers: request.headers,
},
})
response.cookies.set({
name,
value: '',
...options,
})
},
},
}
)
// Get the authenticated user (verifies with Supabase Auth server)
const { data: { user } } = await supabase.auth.getUser()
const isAuthenticated = !!user
const validatedUser = user || null
// Secure audit logging (no sensitive data)
console.log('[AUDIT]', JSON.stringify({
timestamp: new Date().toISOString(),
path: request.nextUrl.pathname,
method: request.method,
isAuthenticated,
userId: validatedUser?.id ? '***masked***' : 'anonymous',
ip: request.ip || 'unknown'
}))
// Public routes that don't require auth
const publicRoutes = ['/', '/login', '/signup', '/forgot-password', '/reset-password', '/auth/callback']
const isPublicRoute = publicRoutes.includes(request.nextUrl.pathname) ||
request.nextUrl.pathname.startsWith('/invite/')
// Special routes that require specific permissions
const isSuperAdminRoute = request.nextUrl.pathname === '/create-new-tenant' ||
request.nextUrl.pathname.startsWith('/dashboard/control-center')
// If not authenticated and trying to access protected route
if (!isAuthenticated && !isPublicRoute) {
console.log('[Middleware] Not authenticated, redirecting to /')
return NextResponse.redirect(new URL('/', request.url))
}
// If authenticated and trying to access public routes (except root)
if (isAuthenticated && request.nextUrl.pathname === '/') {
// Don't redirect from root, let the page handle it
return response
}
// Special handling for dashboard routes
if (isAuthenticated && request.nextUrl.pathname.startsWith('/dashboard')) {
console.log('[Middleware] Dashboard route access for authenticated user')
// First check if user is superadmin
const superAdminUrl = new URL('/api/users/is-superadmin', request.url)
try {
const headers = new Headers()
// Rely on cookies for authentication; do not forward unverified tokens
headers.set('Cookie', request.headers.get('Cookie') || '')
const superAdminResponse = await fetch(superAdminUrl, {
headers,
method: 'GET',
})
if (superAdminResponse.ok) {
const superAdminData = await superAdminResponse.json()
console.log('[Middleware] Superadmin check result:', {
isSuperAdmin: superAdminData.isSuperAdmin
})
// Superadmins manage the platform. If there are no tenants yet, guide them to /tenants
if (superAdminData.isSuperAdmin) {
try {
const allOrgsUrl = new URL('/api/organizations/all', request.url)
const allHeaders = new Headers()
allHeaders.set('Cookie', request.headers.get('Cookie') || '')
const allOrgsResponse = await fetch(allOrgsUrl, {
headers: allHeaders,
method: 'GET',
})
if (allOrgsResponse.ok) {
const allData = await allOrgsResponse.json()
const orgs = Array.isArray(allData.organizations) ? allData.organizations : []
if (orgs.length === 0) {
console.log('[Middleware] Superadmin with no tenants found → redirecting to /tenants')
return NextResponse.redirect(new URL('/tenants', request.url))
}
}
} catch (e) {
console.error('[Middleware] Error checking all organizations for superadmin')
}
console.log('[Middleware] ✅ Allowing superadmin access to dashboard')
return response
}
} else {
console.error('[Middleware] Superadmin check failed with status:', superAdminResponse.status)
return NextResponse.redirect(new URL('/', request.url))
}
} catch (error) {
console.error('[Middleware] Error checking superadmin status:', error instanceof Error ? error.message : 'Unknown error')
// Fail securely - deny access on error
return NextResponse.redirect(new URL('/', request.url))
}
// For non-superadmins, check if they have organizations
const apiUrl = new URL('/api/organizations/check', request.url)
try {
const headers = new Headers()
// Rely on cookies for authentication; do not forward unverified tokens
headers.set('Cookie', request.headers.get('Cookie') || '')
const checkResponse = await fetch(apiUrl, {
headers,
method: 'GET',
})
if (checkResponse.ok) {
const data = await checkResponse.json()
console.log('[Middleware] Organization check result:', {
hasOrganizations: data.hasOrganizations,
isSuperAdmin: data.isSuperAdmin
})
// If non-superadmin has no organizations, show waiting page
// They cannot create tenants - only superadmin can
if (!data.hasOrganizations) {
console.log('[Middleware] ❌ User has no organizations, redirecting to login')
// Redirect to a waiting page or show error
// For now, just block access
return NextResponse.redirect(new URL('/', request.url))
}
} else {
console.error('[Middleware] Organization check failed with status:', checkResponse.status)
return NextResponse.redirect(new URL('/', request.url))
}
} catch (error) {
console.error('[Middleware] Error checking organizations:', error instanceof Error ? error.message : 'Unknown error')
// Fail securely - deny access on error
return NextResponse.redirect(new URL('/', request.url))
}
}
// For create-new-tenant page, ensure user is authenticated AND is superadmin
if (isSuperAdminRoute) {
if (!isAuthenticated) {
console.log('[Middleware] Not authenticated for superadmin route, redirecting to /')
return NextResponse.redirect(new URL('/', request.url))
}
// Check if user is superadmin
const superAdminUrl = new URL('/api/users/is-superadmin', request.url)
try {
const headers = new Headers()
// Rely on cookies for authentication; do not forward unverified tokens
headers.set('Cookie', request.headers.get('Cookie') || '')
const superAdminResponse = await fetch(superAdminUrl, {
headers,
method: 'GET',
})
if (superAdminResponse.ok) {
const superAdminData = await superAdminResponse.json()
// Only superadmins can create tenants
if (!superAdminData.isSuperAdmin) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
} else {
console.error('[Middleware] Superadmin check for tenant creation failed with status:', superAdminResponse.status)
return NextResponse.redirect(new URL('/', request.url))
}
} catch (error) {
console.error('[Middleware] Error checking superadmin for tenant creation:', error instanceof Error ? error.message : 'Unknown error')
// Fail securely - redirect to home instead of dashboard
return NextResponse.redirect(new URL('/', request.url))
}
}
return response
}
// Configure which routes to run middleware on
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes - handle auth in each route instead)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public folder
*/
'/((?!api|_next/static|_next/image|favicon.ico|public).*)',
],
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment