nuxt-supabase-team-auth
Version:
Drop-in Nuxt 3 module for team-based authentication with Supabase
120 lines (101 loc) • 4.15 kB
text/typescript
import { useTeamAuth } from '../composables/useTeamAuth'
import { navigateTo, defineNuxtRouteMiddleware, useRuntimeConfig } from '#imports'
type TeamRole = 'owner' | 'admin' | 'member' | 'super_admin'
/**
* Role hierarchy levels for comparison
*/
const ROLE_HIERARCHY: Record<TeamRole, number> = {
super_admin: 4,
owner: 3,
admin: 2,
member: 1,
}
/**
* Create middleware that requires a specific role or higher
* @param requiredRole The minimum role required
* @param options Additional options
* @param options.redirectTo Path to redirect to when access is denied
* @param options.errorMessage Custom error message to display
* @param options.strict If true, requires exact role match instead of minimum role
*/
export function createRequireRoleMiddleware(
requiredRole: TeamRole,
options: {
redirectTo?: string
errorMessage?: string
strict?: boolean // If true, requires exact role match
} = {},
) {
return defineNuxtRouteMiddleware(async (to) => {
const { currentUser, currentRole, isLoading } = useTeamAuth()
// More efficient auth loading wait with early exit
if (isLoading.value) {
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 role decisions
if (currentUser.value !== undefined && currentRole.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 require-role middleware, proceeding anyway')
}
}
// Ensure user is authenticated
if (!currentUser.value) {
const redirectUrl = `${to.path}${to.search ? `?${new URLSearchParams(to.query).toString()}` : ''}`
const config = useRuntimeConfig()
const loginPage = config.public.teamAuth?.loginPage || '/signin'
return navigateTo(`${loginPage}?redirect=${encodeURIComponent(redirectUrl)}`)
}
// Check if user has a role (requires team membership)
if (!currentRole.value) {
return navigateTo('/teams?message=select_team_first')
}
// Check role permissions
const userRoleLevel = ROLE_HIERARCHY[currentRole.value as TeamRole]
const requiredRoleLevel = ROLE_HIERARCHY[requiredRole]
if (!userRoleLevel || !requiredRoleLevel) {
console.error('Invalid role detected:', { userRole: currentRole.value, requiredRole })
return navigateTo('/dashboard?error=invalid_role')
}
// Check if user has sufficient role level
const hasPermission = options.strict
? userRoleLevel === requiredRoleLevel
: userRoleLevel >= requiredRoleLevel
if (!hasPermission) {
const redirectTo = options.redirectTo || '/dashboard'
const errorParam = options.errorMessage || 'insufficient_permissions'
return navigateTo(`${redirectTo}?error=${errorParam}`)
}
})
}
/**
* Predefined role middleware
*/
export const requireAdmin = createRequireRoleMiddleware('admin')
export const requireOwner = createRequireRoleMiddleware('owner')
export const requireSuperAdmin = createRequireRoleMiddleware('super_admin')
/**
* Strict role middleware (exact role match)
*/
export const requireAdminOnly = createRequireRoleMiddleware('admin', { strict: true })
export const requireOwnerOnly = createRequireRoleMiddleware('owner', { strict: true })
export const requireSuperAdminOnly = createRequireRoleMiddleware('super_admin', { strict: true })
/**
* Default export for dynamic role checking
*/
export default defineNuxtRouteMiddleware(async (to, _from) => {
// This middleware can be used with route meta to specify required role
const requiredRole = to.meta.requireRole as TeamRole
if (!requiredRole) {
console.warn('require-role middleware used without specifying required role in route meta')
return
}
const middleware = createRequireRoleMiddleware(requiredRole)
return middleware(to, _from)
})