nuxt-supabase-team-auth
Version:
Drop-in Nuxt 3 module for team-based authentication with Supabase
127 lines (104 loc) • 4.37 kB
text/typescript
import { useTeamAuth } from '../composables/useTeamAuth'
import { navigateTo, defineNuxtRouteMiddleware, useRuntimeConfig } from '#imports'
/**
* Global authentication middleware
* Runs on every route change to ensure proper authentication state
*/
export default defineNuxtRouteMiddleware(async (to) => {
// Skip middleware on server-side rendering for performance
if (import.meta.server) return
const { currentUser, currentTeam, currentRole, isLoading, isImpersonating } = useTeamAuth()
// More efficient auth loading wait with early exit
if (isLoading.value) {
// Use a shorter timeout with more frequent checks for better responsiveness
let attempts = 0
const maxAttempts = 20 // 2 seconds max (20 * 100ms)
while (isLoading.value && attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 100))
attempts++
// Early exit if we have enough data for basic routing decisions
if (currentUser.value !== undefined) {
break
}
}
// If still loading after timeout, proceed anyway to avoid hanging
if (isLoading.value && attempts >= maxAttempts) {
console.warn('[Team Auth] Auth loading timeout in middleware, proceeding anyway')
}
}
// Get route protection config from runtime config
const config = useRuntimeConfig()
const teamAuthConfig = config.public.teamAuth || {}
const defaultProtection = teamAuthConfig.defaultProtection || 'public'
const configuredProtectedRoutes = teamAuthConfig.protectedRoutes || ['/dashboard']
const configuredPublicRoutes = teamAuthConfig.publicRoutes || []
// Always public auth routes
const authRoutes = [
'/login',
'/signin',
'/signup',
'/auth',
'/confirm',
'/reset-password',
'/forgot-password',
'/accept-invite',
]
// Build final route lists based on protection mode
let protectedRoutes: string[]
let publicRoutes: string[]
if (defaultProtection === 'public') {
// Public by default - only specific routes are protected
protectedRoutes = configuredProtectedRoutes
publicRoutes = ['/', ...authRoutes, ...configuredPublicRoutes]
}
else {
// Protected by default - specific routes are public
protectedRoutes = ['/dashboard', '/team', '/teams', '/admin', '/profile', '/settings']
publicRoutes = ['/', ...authRoutes, ...configuredPublicRoutes]
}
const currentPath = to.path
// Check if current route is public
const isPublicRoute = publicRoutes.some(route =>
currentPath === route || currentPath.startsWith(route + '/'),
)
// Allow access to public routes
if (isPublicRoute) {
return
}
// Check if current route is protected
const isProtectedRoute = protectedRoutes.some(route =>
currentPath.startsWith(route),
)
// Require authentication for protected routes
if (isProtectedRoute && !currentUser.value) {
const redirectUrl = `${currentPath}${to.search ? `?${new URLSearchParams(to.query).toString()}` : ''}`
const loginPage = teamAuthConfig.loginPage || '/signin'
return navigateTo(`${loginPage}?redirect=${encodeURIComponent(redirectUrl)}`)
}
// Handle impersonation restrictions
if (isImpersonating.value) {
// Block admin routes during impersonation (except stop impersonation)
if (currentPath.startsWith('/admin/') && !currentPath.includes('/impersonate/stop')) {
return navigateTo('/dashboard?error=admin_blocked_during_impersonation')
}
}
// Handle super admin impersonation routes
if (currentPath.includes('/admin/impersonate') || currentPath.includes('/impersonate')) {
if (currentRole.value !== 'super_admin') {
return navigateTo('/dashboard?error=insufficient_permissions')
}
}
// Handle team-specific routes
if (currentPath.startsWith('/teams/') && currentPath !== '/teams') {
const teamIdFromRoute = currentPath.split('/teams/')[1]?.split('/')[0]
if (teamIdFromRoute && currentTeam.value?.id !== teamIdFromRoute) {
return navigateTo('/teams?error=unauthorized_team_access')
}
}
// Handle routes that require team membership
const teamRequiredRoutes = ['/team/', '/dashboard']
const requiresTeam = teamRequiredRoutes.some(route => currentPath.startsWith(route))
if (requiresTeam && currentUser.value && !currentTeam.value) {
return navigateTo('/teams?message=select_team_first')
}
})