next-buckler
v1.2.6
Published
π‘οΈ The shield that every Next.js app needs. Effortless authentication and authorization management with menu-based RBAC configuration.
Maintainers
Readme
οΏ½ What's New in v1.2.6
π Menu-Based Configuration - The biggest feature yet!
- π― Single Source of Truth: Define your navigation menu once, auto-generate RBAC configuration
- π 79% Less Code: Reduce ~150-200 lines of boilerplate per project
- π Security Built-In: Automatic validation prevents duplicate routes, role leakage, and misconfigurations
- οΏ½ Flexible Role Assignment: Use
role: 'admin'(single) ORroles: ['admin', 'editor'](multiple) - both formats supported! - οΏ½π NextAuth Integration: Optional
useBucklerSession()hook and role processing helpers - π οΈ Advanced Utilities: Access internal functions like
verifyPath,getGrantedRoutes,isDynamicRoute - π³ Tree-Shakeable: Import only what you need - integrations don't bloat your bundle
See Full Documentation | Before & After Comparison
οΏ½π― Why Next-Buckler?
After implementing authentication and authorization boilerplate across countless projects, I created Next-Buckler to solve these pain points once and for all:
- β No more repetitive auth boilerplate code
- β No hardcoded redirects - state-driven routing
- β Zero flash of unauthenticated content (FOUC)
- β Type-safe routes and roles
- β Works with any auth provider (NextAuth, Auth0, Clerk, custom, etc.)
β¨ Features
- π Role-Based Access Control (RBAC): Manage complex permissions with multiple roles
- π¦ Automatic Route Protection: Smart redirection based on authentication state
- π Conditional Rendering:
BucklerGuardcomponent to show/hide UI based on roles - π Hybrid Routes: Support for routes accessible by both authenticated and non-authenticated users
- π― Menu-Based Configuration: Auto-generate RBAC from menu structure - reduce boilerplate by 79%
- π Helper Utilities: Process menus, group routes, merge configurations with security validation
- π Framework Integrations: Optional NextAuth.js helpers (tree-shakeable)
- π οΈ Advanced Utilities: Export internal functions for custom auth logic
- β‘ Optimized Performance: Automatic memoization and route validation with caching
- π‘οΈ Security First: Built-in protection against path traversal and bypass attacks
- π Monitoring: Callbacks to track unauthorized access attempts
- π― TypeScript First: Full typing with intelligent inference
- πͺΆ Lightweight: ~4KB minified (under 25KB limit)
- π§ Framework Agnostic: Compatible with any authentication solution
π¦ Installation
npm install next-buckler
# or
yarn add next-buckler
# or
pnpm add next-bucklerοΏ½ Import Reference
All exports are available from the main package entry point:
import { /* any export */ } from 'next-buckler'Core Components
| Export | Category | Description |
|--------|----------|-------------|
| Buckler | Component | Main route protection wrapper component |
| BucklerGuard | Component | Conditional rendering component for role-based UI |
Helper Functions (Menu-Based Configuration)
| Export | Category | Description |
|--------|----------|-------------|
| processMenuItems | Helper | Process menu structure with role-based visibility |
| groupRoutesByType | Helper | Auto-generate RBAC configuration from menu |
| mergeGroupedRoutes | Helper | Merge multiple route configurations |
Utility Functions (Advanced)
| Export | Category | Description |
|--------|----------|-------------|
| verifyPath | Utility | Validate paths with security checks (dynamic routes support) |
| isDynamicRoute | Utility | Detect if route contains dynamic segments |
| getAccessRoute | Utility | Determine user's default route based on roles |
| getGrantedRoutes | Utility | Calculate all accessible routes for user |
NextAuth.js Integration (Optional)
| Export | Category | Description |
|--------|----------|-------------|
| useBucklerSession | Hook | NextAuth session hook with Buckler-compatible output |
| processSessionRoles | Function | Extract and normalize roles from NextAuth session |
| createRoleHandler | Factory | Create custom role validation logic |
TypeScript Types
| Export | Category | Description |
|--------|----------|-------------|
| BucklerProps | Type | Props for Buckler component |
| UnauthorizedAccessInfo | Type | Callback info for unauthorized access attempts |
| RoleAccess | Type | RBAC configuration type |
| MenuItem | Type | Menu item structure for menu-based configuration |
| RouteType | Type | Route type union: 'private' \| 'public' \| 'hybrid' |
| GroupedRoutes | Type | Output type of groupRoutesByType() |
| MenuProcessorOptions | Type | Options for menu processing |
| ValidationResult | Type | Validation result structure |
| ProcessedMenu | Type | Output type of processMenuItems() |
| BucklerSession | Type | NextAuth session type for Buckler |
| SessionProcessorOptions | Type | Options for session role processing |
Note: All integrations are tree-shakeable. NextAuth helpers are only included in your bundle if you import them.
οΏ½π Quick Start
Step 1: Import Components
import { Buckler, BucklerGuard } from 'next-buckler'Step 2: Wrap Your App
Update your _app.tsx to protect your routes:
// pages/_app.tsx
import type { AppProps } from 'next/app'
import { useRouter } from 'next/router'
import { Buckler } from 'next-buckler'
import { useAuth } from './hooks/useAuth' // Your auth hook
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter()
const { isAuthenticated, isLoading } = useAuth()
return (
<Buckler
isAuth={isAuthenticated}
isLoading={isLoading}
router={router}
loginRoute="/login"
defaultRoute="/dashboard"
privateRoutes={['/dashboard', '/profile', '/settings']}
publicRoutes={['/login', '/register', '/']}
LoadingComponent={<div>Loading...</div>}
>
<Component {...pageProps} />
</Buckler>
)
}
export default MyAppStep 3: Use BucklerGuard for Conditional UI
// components/AdminPanel.tsx
import { BucklerGuard } from 'next-buckler'
export function AdminPanel() {
return (
<div>
<h1>Dashboard</h1>
<BucklerGuard
RBAC
showForRole="admin"
userRoles={['admin']}
fallback={<p>Access denied</p>}
>
<AdminContent />
</BucklerGuard>
</div>
)
}π Complete Implementation Examples
Example 1: Basic Authentication (No RBAC)
Perfect for apps with simple authenticated/non-authenticated states.
// pages/_app.tsx
import type { AppProps } from 'next/app'
import { useRouter } from 'next/router'
import { Buckler } from 'next-buckler'
import { useSession } from 'next-auth/react'
// Define your routes as constants for type safety
const PRIVATE_ROUTES = [
'/dashboard',
'/profile',
'/settings',
'/projects',
'/projects/[id]'
] as const
const PUBLIC_ROUTES = [
'/',
'/login',
'/register',
'/about',
'/pricing'
] as const
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter()
const { data: session, status } = useSession()
const isAuthenticated = !!session
const isLoading = status === 'loading'
return (
<Buckler
isAuth={isAuthenticated}
isLoading={isLoading}
router={router}
loginRoute="/login"
defaultRoute="/dashboard"
privateRoutes={PRIVATE_ROUTES}
publicRoutes={PUBLIC_ROUTES}
LoadingComponent={
<div className="flex h-screen items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900" />
</div>
}
>
<Component {...pageProps} />
</Buckler>
)
}
export default MyAppWhat happens:
- β
Unauthenticated users accessing
/dashboardβ Redirected to/login - β
Authenticated users accessing
/loginβ Redirected to/dashboard - β No flash of content during authentication check
- β Loading spinner shown during state transitions
Example 2: Role-Based Access Control (RBAC)
For apps with multiple user roles and complex permissions.
// pages/_app.tsx
import type { AppProps } from 'next/app'
import { useRouter } from 'next/router'
import { Buckler } from 'next-buckler'
import { useAuth } from '@/hooks/useAuth'
// Define routes with type safety
const PRIVATE_ROUTES = [
'/dashboard',
'/admin',
'/admin/users',
'/admin/settings',
'/editor',
'/editor/posts',
'/profile'
] as const
const PUBLIC_ROUTES = [
'/',
'/login',
'/register'
] as const
// Define RBAC configuration
const RBAC_CONFIG = {
admin: {
accessRoute: '/admin',
grantedRoutes: [
'/dashboard',
'/admin',
'/admin/users',
'/admin/settings',
'/editor',
'/editor/posts',
'/profile'
]
},
editor: {
accessRoute: '/editor',
grantedRoutes: [
'/dashboard',
'/editor',
'/editor/posts',
'/profile'
]
},
user: {
accessRoute: '/dashboard',
grantedRoutes: [
'/dashboard',
'/profile'
]
}
} as const
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter()
const { user, isAuthenticated, isLoading } = useAuth()
return (
<Buckler
isAuth={isAuthenticated}
isLoading={isLoading}
router={router}
loginRoute="/login"
defaultRoute="/dashboard"
privateRoutes={PRIVATE_ROUTES}
publicRoutes={PUBLIC_ROUTES}
RBAC={RBAC_CONFIG}
userRoles={user?.roles} // e.g., ['admin', 'editor']
strictMode={process.env.NODE_ENV !== 'production'}
onUnauthorizedAccess={(info) => {
// Log unauthorized access attempts
console.warn('Unauthorized access attempt:', info)
// Send to analytics
analytics.track('unauthorized_access', {
path: info.path,
reason: info.reason,
userRoles: info.userRoles
})
}}
LoadingComponent={<LoadingSpinner />}
>
<Component {...pageProps} />
</Buckler>
)
}
export default MyAppWhat happens:
- β
Admin accessing
/editorβ β Allowed (admin has access) - β
Editor accessing
/adminβ β Redirected to/editor(insufficient permissions) - β
User accessing
/editorβ β Redirected to/dashboard(insufficient permissions) - β
Each role automatically redirected to their designated
accessRoute - β Unauthorized attempts logged via callback
Example 3: Hybrid Routes
Routes that are accessible to both authenticated and non-authenticated users.
// pages/_app.tsx
import { Buckler } from 'next-buckler'
const PRIVATE_ROUTES = ['/dashboard', '/settings'] as const
const PUBLIC_ROUTES = ['/login', '/register'] as const
const HYBRID_ROUTES = ['/blog', '/blog/[slug]', '/docs', '/about'] as const
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter()
const { isAuthenticated, isLoading } = useAuth()
return (
<Buckler
isAuth={isAuthenticated}
isLoading={isLoading}
router={router}
loginRoute="/login"
defaultRoute="/dashboard"
privateRoutes={PRIVATE_ROUTES}
publicRoutes={PUBLIC_ROUTES}
hybridRoutes={HYBRID_ROUTES} // β No redirects for these routes
LoadingComponent={<LoadingSpinner />}
>
<Component {...pageProps} />
</Buckler>
)
}Use cases for hybrid routes:
- Blog posts accessible to everyone, with extra features for logged-in users
- Documentation pages
- Landing pages with conditional content
- Marketing pages
Example 4: Conditional UI with BucklerGuard
Control what UI elements users see based on their roles.
// components/Dashboard.tsx
import { BucklerGuard } from 'next-buckler'
import { useAuth } from '@/hooks/useAuth'
export function Dashboard() {
const { user } = useAuth()
return (
<div className="container">
<h1>Dashboard</h1>
{/* Show admin panel only to admins */}
<BucklerGuard
RBAC
showForRole="admin"
userRoles={user?.roles}
fallback={null}
>
<AdminPanel />
</BucklerGuard>
{/* Show editor tools to editors and admins */}
<BucklerGuard
RBAC
showForRole="editor"
userRoles={user?.roles}
fallback={<p className="text-gray-500">Upgrade to editor to access these tools</p>}
>
<EditorTools />
</BucklerGuard>
{/* Simple conditional rendering */}
<BucklerGuard
showIf={user?.isPremium}
fallback={<UpgradePrompt />}
>
<PremiumFeatures />
</BucklerGuard>
{/* Always visible content */}
<UserProfile user={user} />
</div>
)
}Multiple guards in action:
// components/Navbar.tsx
import { BucklerGuard } from 'next-buckler'
export function Navbar({ userRoles }: { userRoles?: string[] }) {
return (
<nav>
<Logo />
{/* Admin link */}
<BucklerGuard RBAC showForRole="admin" userRoles={userRoles}>
<NavLink href="/admin">Admin Panel</NavLink>
</BucklerGuard>
{/* Editor link */}
<BucklerGuard RBAC showForRole="editor" userRoles={userRoles}>
<NavLink href="/editor">Editor</NavLink>
</BucklerGuard>
{/* User link (everyone) */}
<NavLink href="/dashboard">Dashboard</NavLink>
<NavLink href="/profile">Profile</NavLink>
</nav>
)
}Example 5: With NextAuth.js
// pages/_app.tsx
import { SessionProvider, useSession } from 'next-auth/react'
import { Buckler } from 'next-buckler'
function BucklerWrapper({ Component, pageProps }: any) {
const router = useRouter()
const { data: session, status } = useSession()
return (
<Buckler
isAuth={!!session}
isLoading={status === 'loading'}
router={router}
loginRoute="/login"
defaultRoute="/dashboard"
privateRoutes={['/dashboard', '/profile']}
publicRoutes={['/login', '/']}
RBAC={{
admin: {
accessRoute: '/admin',
grantedRoutes: ['/admin', '/dashboard', '/profile']
},
user: {
accessRoute: '/dashboard',
grantedRoutes: ['/dashboard', '/profile']
}
}}
userRoles={session?.user?.roles}
LoadingComponent={<div>Loading...</div>}
>
<Component {...pageProps} />
</Buckler>
)
}
function MyApp({ Component, pageProps }: AppProps) {
return (
<SessionProvider session={pageProps.session}>
<BucklerWrapper Component={Component} pageProps={pageProps} />
</SessionProvider>
)
}
export default MyAppExample 6: With Custom Auth Hook
// hooks/useAuth.ts
import { useState, useEffect } from 'react'
export function useAuth() {
const [user, setUser] = useState(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
// Fetch user from your API
fetch('/api/auth/me')
.then(res => res.json())
.then(data => {
setUser(data.user)
setIsLoading(false)
})
.catch(() => {
setUser(null)
setIsLoading(false)
})
}, [])
return {
user,
isAuthenticated: !!user,
isLoading,
}
}
// pages/_app.tsx
import { useAuth } from '@/hooks/useAuth'
import { Buckler } from 'next-buckler'
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter()
const { user, isAuthenticated, isLoading } = useAuth()
return (
<Buckler
isAuth={isAuthenticated}
isLoading={isLoading}
router={router}
loginRoute="/login"
defaultRoute="/dashboard"
privateRoutes={['/dashboard', '/profile']}
publicRoutes={['/login', '/register']}
userRoles={user?.roles}
LoadingComponent={<div>Loading...</div>}
>
<Component {...pageProps} />
</Buckler>
)
}π― Menu-Based Configuration (NEW in v1.2.6)
The Problem: Most applications define navigation menus AND route permissions separately, leading to duplicate code and sync issues.
The Solution: Define your menu structure once with metadata, then automatically generate RBAC configuration and apply role-based visibility.
πͺ Why Menu-Based Configuration?
β
Single Source of Truth - Define routes and permissions in one place
β
Reduce Boilerplate - Eliminate ~150-200 lines of repetitive configuration
β
Auto-Generated RBAC - Generate route permissions from menu structure
β
Type-Safe - Full TypeScript support with intelligent inference
β
Security Built-In - Automatic validation prevents configuration errors
β
Framework Integration - Optional NextAuth.js helpers included
π Before & After Comparison
// routes.ts
export const menuItems = [
{ path: '/', label: 'Home', icon: 'HomeIcon' },
{ path: '/dashboard', label: 'Dashboard', icon: 'DashboardIcon' },
{ path: '/admin', label: 'Admin', icon: 'AdminIcon' },
{ path: '/admin/users', label: 'Users', icon: 'UsersIcon' },
{ path: '/editor', label: 'Editor', icon: 'EditorIcon' },
]
// config/rbac.ts
export const RBAC_CONFIG = {
admin: {
accessRoute: '/admin',
grantedRoutes: ['/dashboard', '/admin', '/admin/users', '/editor', '/profile']
},
editor: {
accessRoute: '/editor',
grantedRoutes: ['/dashboard', '/editor', '/profile']
},
user: {
accessRoute: '/dashboard',
grantedRoutes: ['/dashboard', '/profile']
}
}
export const PRIVATE_ROUTES = ['/dashboard', '/admin', '/admin/users', '/editor', '/profile']
export const PUBLIC_ROUTES = ['/', '/login', '/register']
// helpers/menus.ts
export function processMenuItems(items: MenuItem[], userRoles: string[]) {
return items.filter(item => {
if (item.roles) {
return item.roles.some(role => userRoles.includes(role))
}
return true
}).map(item => ({
...item,
children: item.children ? processMenuItems(item.children, userRoles) : undefined
}))
}
export function groupRoutesByType(items: MenuItem[]) {
const privateRoutes: string[] = []
const publicRoutes: string[] = []
function extract(items: MenuItem[]) {
items.forEach(item => {
if (item.type === 'private') privateRoutes.push(item.path)
if (item.type === 'public') publicRoutes.push(item.path)
if (item.children) extract(item.children)
})
}
extract(items)
return { privateRoutes, publicRoutes }
}
// pages/_app.tsx
import { RBAC_CONFIG, PRIVATE_ROUTES, PUBLIC_ROUTES } from '@/config/routes'
import { processMenuItems } from '@/helpers/menus'
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter()
const { data: session } = useSession()
const visibleMenu = processMenuItems(menuItems, session?.user?.roles || [])
return (
<Buckler
isAuth={!!session}
isLoading={status === 'loading'}
router={router}
loginRoute="/login"
defaultRoute="/dashboard"
privateRoutes={PRIVATE_ROUTES}
publicRoutes={PUBLIC_ROUTES}
RBAC={RBAC_CONFIG}
userRoles={session?.user?.roles}
>
<Layout menu={visibleMenu}>
<Component {...pageProps} />
</Layout>
</Buckler>
)
}Total: ~165 lines + manual sync between menu and RBAC config
// config/menu.ts
import type { MenuItem } from 'next-buckler'
export const menuConfig: MenuItem[] = [
{
path: '/',
label: 'Home',
icon: 'HomeIcon',
type: 'public'
},
{
path: '/dashboard',
label: 'Dashboard',
icon: 'DashboardIcon',
type: 'private',
roles: ['user', 'editor', 'admin']
},
{
path: '/admin',
label: 'Admin',
icon: 'AdminIcon',
type: 'private',
roles: ['admin'],
children: [
{
path: '/admin/users',
label: 'Manage Users',
icon: 'UsersIcon',
type: 'private',
roles: ['admin']
}
]
},
{
path: '/editor',
label: 'Editor',
icon: 'EditorIcon',
type: 'private',
roles: ['editor', 'admin']
}
]
// pages/_app.tsx
import { Buckler, processMenuItems, groupRoutesByType, useBucklerSession } from 'next-buckler'
import { menuConfig } from '@/config/menu'
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter()
const { isAuth, isLoading, userRoles } = useBucklerSession()
// Process menu for visibility
const { visibleItems: visibleMenu } = processMenuItems(menuConfig, userRoles)
// Auto-generate route configuration
const { privateRoutes, publicRoutes, rbacConfig } = groupRoutesByType(menuConfig)
return (
<Buckler
isAuth={isAuth}
isLoading={isLoading}
router={router}
loginRoute="/login"
defaultRoute="/dashboard"
privateRoutes={privateRoutes}
publicRoutes={publicRoutes}
RBAC={rbacConfig}
userRoles={userRoles}
>
<Layout menu={visibleMenu}>
<Component {...pageProps} />
</Layout>
</Buckler>
)
}Total: ~35 lines - Everything auto-generated, always in sync
Result: 130 lines eliminated (79% reduction), zero sync issues, one source of truth! π
π Complete Menu-Based Implementation
Step 1: Define Your Menu Structure
// config/menu.ts
import type { MenuItem } from 'next-buckler'
export const menuConfig: MenuItem[] = [
// Public routes
{
path: '/',
label: 'Home',
icon: 'HomeIcon',
type: 'public'
},
{
path: '/about',
label: 'About',
icon: 'InfoIcon',
type: 'public'
},
// Private routes with roles
{
path: '/dashboard',
label: 'Dashboard',
icon: 'DashboardIcon',
type: 'private',
roles: ['user', 'editor', 'admin'],
children: [
{
path: '/dashboard/analytics',
label: 'Analytics',
icon: 'ChartIcon',
type: 'private',
roles: ['editor', 'admin']
}
]
},
// Admin section
{
path: '/admin',
label: 'Administration',
icon: 'ShieldIcon',
type: 'private',
roles: ['admin'],
children: [
{
path: '/admin/users',
label: 'User Management',
icon: 'UsersIcon',
type: 'private',
roles: ['admin']
},
{
path: '/admin/settings',
label: 'Settings',
icon: 'SettingsIcon',
type: 'private',
roles: ['admin']
}
]
},
// Hybrid route (accessible to all)
{
path: '/blog',
label: 'Blog',
icon: 'BookIcon',
type: 'hybrid' // Both authenticated and non-authenticated users
}
]Step 2: Use in Your App
// pages/_app.tsx
import type { AppProps } from 'next/app'
import { useRouter } from 'next/router'
import { SessionProvider } from 'next-auth/react'
import {
Buckler,
processMenuItems,
groupRoutesByType,
useBucklerSession
} from 'next-buckler'
import { menuConfig } from '@/config/menu'
import Layout from '@/components/Layout'
function App({ Component, pageProps }: AppProps) {
const router = useRouter()
const { isAuth, isLoading, userRoles } = useBucklerSession()
// Process menu items - automatically filters based on roles
const { visibleItems, hiddenItems } = processMenuItems(menuConfig, userRoles, {
strictMode: process.env.NODE_ENV !== 'production'
})
// Auto-generate route configuration from menu
const { privateRoutes, publicRoutes, hybridRoutes, rbacConfig } = groupRoutesByType(
menuConfig,
{ strictMode: process.env.NODE_ENV !== 'production' }
)
return (
<Buckler
isAuth={isAuth}
isLoading={isLoading}
router={router}
loginRoute="/login"
defaultRoute="/dashboard"
privateRoutes={privateRoutes}
publicRoutes={publicRoutes}
hybridRoutes={hybridRoutes}
RBAC={rbacConfig}
userRoles={userRoles}
strictMode={process.env.NODE_ENV !== 'production'}
onUnauthorizedAccess={(info) => {
console.warn('[Security] Unauthorized access:', info)
}}
LoadingComponent={
<div className="flex h-screen items-center justify-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-600" />
</div>
}
>
<Layout navigation={visibleItems}>
<Component {...pageProps} />
</Layout>
</Buckler>
)
}
function MyApp(appProps: AppProps) {
return (
<SessionProvider session={appProps.pageProps.session}>
<App {...appProps} />
</SessionProvider>
)
}
export default MyAppStep 3: Use the Menu in Your Layout
// components/Layout.tsx
import Link from 'next/link'
import { MenuItem } from 'next-buckler'
interface LayoutProps {
navigation: MenuItem[]
children: React.ReactNode
}
export default function Layout({ navigation, children }: LayoutProps) {
return (
<div className="min-h-screen">
<nav className="bg-gray-800 text-white">
<div className="container mx-auto px-4">
<ul className="flex space-x-4 py-4">
{navigation.map((item) => (
<li key={item.path}>
<Link href={item.path} className="hover:text-blue-400">
{item.icon && <span className="mr-2">{item.icon}</span>}
{item.label}
</Link>
{/* Render nested menu items */}
{item.children && item.children.length > 0 && (
<ul className="ml-4 mt-2">
{item.children.map((child) => (
<li key={child.path}>
<Link href={child.path} className="text-sm hover:text-blue-400">
{child.label}
</Link>
</li>
))}
</ul>
)}
</li>
))}
</ul>
</div>
</nav>
<main className="container mx-auto px-4 py-8">
{children}
</main>
</div>
)
}π Menu-Based API Reference
MenuItem<TIcon> Type
interface MenuItem<TIcon = any> {
path: string
label: string
icon?: TIcon
type: 'private' | 'public' | 'hybrid'
role?: string | null // Single role (alternative to roles array)
roles?: string[] | null // Multiple roles (alternative to role)
hidden?: boolean | null // Explicit visibility control
children?: MenuItem<TIcon>[]
metadata?: Record<string, any>
}| Property | Type | Required | Description |
|----------|------|----------|-------------|
| path | string \| null | β
| Route path (supports Next.js dynamic routes, null for parent-only items) |
| label | string | β
| Display label for the menu item |
| icon | TIcon | β | Icon component or identifier (generic type) |
| type | 'private' \| 'public' \| 'hybrid' | β
| Route access type |
| role | string \| null | β | Single role - Only this role can access (use role OR roles, not both) |
| roles | string[] \| null | β | Multiple roles - Any of these roles can access (use role OR roles, not both) |
| hidden | boolean \| null | β | Explicit visibility: true = hidden, false = visible, null = force visible |
| children | MenuItem<TIcon>[] | β | Nested menu items |
| metadata | Record<string, any> | β | Custom metadata for your app |
Role Configuration:
You can use either role (singular) or roles (array), depending on your needs:
// Option 1: Single role
{
path: '/admin',
label: 'Admin Panel',
type: 'private',
role: 'admin' // Only 'admin' role can access
}
// Option 2: Multiple roles (any role grants access)
{
path: '/dashboard',
label: 'Dashboard',
type: 'private',
roles: ['admin', 'editor', 'viewer'] // Any of these roles can access
}
// Option 3: No role (all authenticated users for private, or public)
{
path: '/profile',
label: 'My Profile',
type: 'private',
role: null // All authenticated users can access
}processMenuItems() Function
Process menu structure and apply role-based visibility.
function processMenuItems<TIcon = any>(
items: MenuItem<TIcon>[],
userRoles?: string[],
options?: MenuProcessorOptions
): ProcessedMenu<TIcon>Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| items | MenuItem<TIcon>[] | Menu structure to process |
| userRoles | string[] | Current user's roles (undefined for non-authenticated) |
| options | MenuProcessorOptions | Configuration options |
Options:
interface MenuProcessorOptions {
strictMode?: boolean // Throw errors vs warnings (default: false)
maxDepth?: number // Maximum nesting depth (default: 5)
removeEmptyParents?: boolean // Remove parents with no visible children (default: false)
}Returns:
interface ProcessedMenu<TIcon> {
visibleItems: MenuItem<TIcon>[] // Items the user can see
hiddenItems: MenuItem<TIcon>[] // Items hidden from user
validationErrors: string[] // Any validation errors found
}Example:
const { visibleItems, hiddenItems, validationErrors } = processMenuItems(
menuConfig,
['editor'],
{ strictMode: true, removeEmptyParents: true }
)
if (validationErrors.length > 0) {
console.error('Menu validation errors:', validationErrors)
}
// Use visibleItems in your navigation
<Navigation items={visibleItems} />ValidationResult Type
Validation result returned by menu processing functions.
interface ValidationResult {
valid: boolean // Overall validation status
errors: string[] // Critical validation errors
warnings: string[] // Non-critical warnings
}| Property | Type | Description |
|----------|------|-------------|
| valid | boolean | true if validation passed, false if errors found |
| errors | string[] | Array of critical validation errors |
| warnings | string[] | Array of non-critical warnings |
Example:
import { processMenuItems, ValidationResult } from 'next-buckler'
const result = processMenuItems(menuConfig, userRoles, { strictMode: true })
// Check validation results
if (!result.validation.valid) {
console.error('Menu validation failed:', result.validation.errors)
}
if (result.validation.warnings.length > 0) {
console.warn('Menu validation warnings:', result.validation.warnings)
}groupRoutesByType() Function
Generate route configuration and RBAC config from menu structure.
function groupRoutesByType<TIcon = any>(
items: MenuItem<TIcon>[],
options?: { strictMode?: boolean }
): GroupedRoutesParameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| items | MenuItem<TIcon>[] | Menu structure to process |
| options | { strictMode?: boolean } | Throw errors vs warnings |
Returns:
interface GroupedRoutes {
privateRoutes: string[] // All private routes extracted
publicRoutes: string[] // All public routes extracted
hybridRoutes: string[] // All hybrid routes extracted
rbacConfig: RoleAccess // Auto-generated RBAC configuration
validationErrors: string[] // Security validation errors
}Generated RBAC Config Format:
// For menu with roles: ['admin', 'editor', 'user']
{
admin: {
accessRoute: '/admin', // First route for this role
grantedRoutes: ['/admin', '/dashboard', ...]
},
editor: {
accessRoute: '/editor',
grantedRoutes: ['/editor', '/dashboard', ...]
},
user: {
accessRoute: '/dashboard',
grantedRoutes: ['/dashboard', ...]
}
}Security Validations:
- β Detects duplicate routes across types
- β Validates role references are consistent
- β Prevents private routes from leaking into public
- β Normalizes paths (removes trailing slashes, path traversal)
- β Validates route patterns
Example:
const {
privateRoutes,
publicRoutes,
rbacConfig,
validationErrors
} = groupRoutesByType(menuConfig, { strictMode: true })
if (validationErrors.length > 0) {
throw new Error(`Route configuration errors: ${validationErrors.join(', ')}`)
}
// Use in Buckler
<Buckler
privateRoutes={privateRoutes}
publicRoutes={publicRoutes}
RBAC={rbacConfig}
// ...
/>mergeGroupedRoutes() Function
Merge multiple route configurations (useful for modular apps).
function mergeGroupedRoutes(...configs: GroupedRoutes[]): GroupedRoutesExample:
// Feature 1 routes
const blogRoutes = groupRoutesByType(blogMenuConfig)
// Feature 2 routes
const adminRoutes = groupRoutesByType(adminMenuConfig)
// Merge them
const allRoutes = mergeGroupedRoutes(blogRoutes, adminRoutes)
<Buckler
privateRoutes={allRoutes.privateRoutes}
publicRoutes={allRoutes.publicRoutes}
RBAC={allRoutes.rbacConfig}
// ...
/>π NextAuth.js Integration
Next-Buckler provides optional helpers for NextAuth.js integration.
useBucklerSession() Hook
Wraps NextAuth's useSession with Buckler-compatible output.
import { useBucklerSession } from 'next-buckler'
function MyComponent() {
const { isAuth, isLoading, userRoles, session } = useBucklerSession()
// isAuth: boolean
// isLoading: boolean
// userRoles: string[] | undefined
// session: Session | null (original NextAuth session)
return <div>User roles: {userRoles?.join(', ')}</div>
}Features:
- β Automatically extracts roles from NextAuth session
- β
Handles role prefixes (e.g.,
app-role-adminβadmin) - β
Provides default
'user'role for authenticated users without roles - β
Returns
undefinedfor non-authenticated users
processSessionRoles() Function
Extract and normalize roles from NextAuth session.
import { processSessionRoles } from 'next-buckler'
import { useSession } from 'next-auth/react'
function MyApp() {
const { data: session } = useSession()
const userRoles = processSessionRoles(session)
// userRoles: string[] | undefined
}Handles Multiple Formats:
// Azure AD format
session.user.roles = ['app-role-admin', 'app-role-editor']
// β ['admin', 'editor']
// Simple array
session.user.roles = ['admin', 'user']
// β ['admin', 'user']
// Nested roles
session.accessToken.roles = ['admin']
// β ['admin']
// No session
null
// β undefined
// Authenticated but no roles
session.user = {}
// β ['user'] (default fallback)Options:
processSessionRoles() accepts an optional SessionProcessorOptions parameter:
interface SessionProcessorOptions {
anonymousRole?: string // Role for unauthenticated users (default: 'anonymous')
defaultRole?: string // Role for authenticated users without specific roles (default: 'user')
rolePrefix?: string // Prefix for role normalization (e.g., 'app-role-')
roleFilter?: (role: string) => boolean // Filter function to validate roles
minValidRoles?: number // Minimum valid roles required (default: 1)
}| Property | Type | Default | Description |
|----------|------|---------|-------------|
| anonymousRole | string | 'anonymous' | Role assigned to unauthenticated users |
| defaultRole | string | 'user' | Fallback role for authenticated users without specific roles |
| rolePrefix | string | '' | Prefix for role normalization (e.g., 'app-role-' β 'admin' becomes 'app-role-admin') |
| roleFilter | function | () => true | Filter function to validate roles before processing |
| minValidRoles | number | 1 | If user has fewer valid roles, defaultRole is used |
Example with Options:
import { processSessionRoles } from 'next-buckler'
const userRoles = processSessionRoles(session, {
rolePrefix: 'app-role-',
roleFilter: (role) => role.startsWith('app-role-'),
defaultRole: 'viewer',
minValidRoles: 1
})
// Session with roles: ['app-role-admin', 'app-role-editor']
// β ['app-role-admin', 'app-role-editor']
// Session with invalid roles: ['guest', 'temp']
// β ['viewer'] (fallback to defaultRole)createRoleHandler() Function
Create custom role validation logic with priority rules.
import { createRoleHandler } from 'next-buckler'
const roleHandler = createRoleHandler({
roleOrder: ['super-admin', 'admin', 'editor', 'user'],
fallbackRole: 'guest',
filterRoles: (roles) => roles.filter(r => r.startsWith('app-')),
})
const { data: session } = useSession()
const userRoles = roleHandler(session)Options:
interface RoleHandlerOptions {
roleOrder?: string[] // Priority order for roles
fallbackRole?: string // Default when no roles found
filterRoles?: (roles: string[]) => string[] // Custom role filtering
}π Advanced Patterns
Pattern 1: Multi-Module Configuration
// modules/admin/menu.ts
export const adminMenu: MenuItem[] = [
{ path: '/admin', label: 'Admin', type: 'private', roles: ['admin'] }
]
// modules/blog/menu.ts
export const blogMenu: MenuItem[] = [
{ path: '/blog', label: 'Blog', type: 'hybrid' }
]
// config/menu.ts
import { adminMenu } from '@/modules/admin/menu'
import { blogMenu } from '@/modules/blog/menu'
import { mergeGroupedRoutes, groupRoutesByType } from 'next-buckler'
const adminConfig = groupRoutesByType(adminMenu)
const blogConfig = groupRoutesByType(blogMenu)
export const appConfig = mergeGroupedRoutes(adminConfig, blogConfig)Pattern 2: Conditional Menu Items
// config/menu.ts
export function getMenuConfig(features: FeatureFlags): MenuItem[] {
const baseMenu: MenuItem[] = [
{ path: '/dashboard', label: 'Dashboard', type: 'private', roles: ['user'] }
]
if (features.enableAdmin) {
baseMenu.push({
path: '/admin',
label: 'Admin',
type: 'private',
roles: ['admin']
})
}
return baseMenu
}
// pages/_app.tsx
const menuConfig = getMenuConfig(featureFlags)
const { privateRoutes, rbacConfig } = groupRoutesByType(menuConfig)Pattern 3: Icon Type Safety
// Use with your icon library
import { Home, Settings, Users } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
const menuConfig: MenuItem<LucideIcon>[] = [
{
path: '/dashboard',
label: 'Dashboard',
icon: Home, // β
Type-safe
type: 'private',
roles: ['user']
},
{
path: '/settings',
label: 'Settings',
icon: Settings, // β
Type-safe
type: 'private',
roles: ['user']
}
]
// In your component
{navigation.map(item => {
const Icon = item.icon
return <Icon key={item.path} /> // β
Fully typed
})}π Key Advantages
1. No More Authentication Boilerplate
Without Next-Buckler:
// Every protected page needs this boilerplate
function DashboardPage() {
const router = useRouter()
const { user, loading } = useAuth()
useEffect(() => {
if (!loading && !user) {
router.push('/login')
}
}, [user, loading, router])
if (loading) return <Loading />
if (!user) return null // Prevents flash
return <Dashboard />
}With Next-Buckler:
// Just wrap once in _app.tsx, all routes protected automatically
function DashboardPage() {
return <Dashboard /> // That's it!
}Result: Remove hundreds of lines of repetitive code across your app.
2. Zero Flash of Unauthenticated Content (FOUC)
Next-Buckler handles the authentication state transitions intelligently:
<Buckler
isAuth={isAuthenticated}
isLoading={isLoading}
LoadingComponent={<Spinner />} // Shown during checks
// ...
>- While checking auth status β Shows loading component
- Invalid access detected β Instant redirect before render
- No flash, no flicker, professional UX
3. Type-Safe Routes
Define routes once, get autocomplete and type checking:
const PRIVATE_ROUTES = ['/dashboard', '/admin'] as const
const PUBLIC_ROUTES = ['/login', '/'] as const
<Buckler
loginRoute="/login" // β
Autocomplete from PUBLIC_ROUTES
defaultRoute="/dashboard" // β
Autocomplete from PRIVATE_ROUTES
privateRoutes={PRIVATE_ROUTES}
publicRoutes={PUBLIC_ROUTES}
/>TypeScript will catch typos and invalid route references at compile time.
4. Dynamic Routes Support
Next-Buckler understands Next.js dynamic routes:
const PRIVATE_ROUTES = [
'/users/[id]', // /users/123 β
'/posts/[...slug]', // /posts/a/b/c β
'/shop/[[...slug]]', // /shop OR /shop/a/b β
]Automatic matching with security built-in (prevents path traversal attacks).
5. Complex RBAC Made Simple
Manage multi-role, multi-permission systems effortlessly:
const RBAC = {
'super-admin': {
accessRoute: '/super-admin',
grantedRoutes: ['/admin', '/editor', '/dashboard', '/super-admin']
},
'admin': {
accessRoute: '/admin',
grantedRoutes: ['/admin', '/editor', '/dashboard']
},
'editor': {
accessRoute: '/editor',
grantedRoutes: ['/editor', '/dashboard']
},
'viewer': {
accessRoute: '/dashboard',
grantedRoutes: ['/dashboard']
}
}Users can have multiple roles - Next-Buckler checks all of them automatically.
6. Security Monitoring
Track and log unauthorized access attempts:
<Buckler
onUnauthorizedAccess={(info) => {
// Log to your monitoring service
Sentry.captureMessage('Unauthorized access attempt', {
extra: {
path: info.path,
reason: info.reason, // 'not_authenticated' | 'insufficient_permissions'
userRoles: info.userRoles,
timestamp: info.timestamp
}
})
}}
// ...
/>Perfect for security audits and analytics.
7. Framework Agnostic
Works with any authentication provider:
- β NextAuth.js
- β Auth0
- β Clerk
- β Firebase Auth
- β Supabase Auth
- β Custom JWT auth
- β Cookie-based auth
- β Session-based auth
Just provide isAuth and isLoading states.
8. Performance Optimized
// Internal optimizations:
// - Route validation results are cached
// - Path calculations are memoized
// - Regex patterns compiled once
// - No unnecessary re-rendersYour auth checks don't slow down your app.
9. Developer Experience
// Strict mode for development
<Buckler
strictMode={process.env.NODE_ENV !== 'production'}
// Throws errors for misconfigurations in dev
// Logs warnings in production
// ...
/>Catch configuration errors early in development.
10. Flexible UI Control
// Hide features for non-premium users
<BucklerGuard showIf={user.isPremium} fallback={<UpgradeButton />}>
<PremiumFeature />
</BucklerGuard>
// Show different content based on role
<BucklerGuard RBAC showForRole="admin" userRoles={roles} fallback={<StandardView />}>
<AdminView />
</BucklerGuard>Granular control over what users see.
π API Reference
Buckler Component
Main wrapper component for route protection.
Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| isAuth | boolean | β
| Whether the user is authenticated |
| isLoading | boolean | β
| Whether auth state is loading |
| router | NextRouter | β
| Next.js router instance |
| loginRoute | string | β
| Route to redirect unauthenticated users |
| defaultRoute | string | β
| Default route for authenticated users |
| privateRoutes | string[] | β
| Array of protected routes |
| publicRoutes | string[] | β
| Array of public routes |
| hybridRoutes | string[] | β | Routes accessible to all users |
| LoadingComponent | ReactNode | β
| Component shown during loading |
| RBAC | RoleAccess | β | RBAC configuration object |
| userRoles | string[] | β | Current user's roles (required if RBAC is used) |
| accessRoute | string | β | Override access route (not used with RBAC) |
| strictMode | boolean | β | Throw errors instead of warnings (default: false) |
| onUnauthorizedAccess | function | β | Callback for unauthorized access attempts |
RBAC Configuration Type
type RoleAccess<Routes extends string[]> = {
[roleName: string]: {
accessRoute?: string // Where to redirect this role on login
grantedRoutes: Routes // Routes this role can access
}
}UnauthorizedAccessInfo Type
type UnauthorizedAccessInfo = {
path: string // Attempted path
userRoles: string[] | undefined
timestamp: Date
reason: 'not_authenticated' | 'insufficient_permissions' | 'invalid_route'
}BucklerGuard Component
Conditional rendering component for UI elements.
Props (Variant 1: Simple Conditional)
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| showIf | boolean | β
| Condition to show children |
| fallback | ReactNode | β | Content to show when condition is false (default: null) |
| children | ReactNode | β
| Content to show when condition is true |
Props (Variant 2: RBAC)
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| RBAC | true | β
| Enable RBAC mode |
| showForRole | string | β
| Role required to see children |
| userRoles | string[] | β
| Current user's roles |
| fallback | ReactNode | β | Content to show when role not matched (default: null) |
| children | ReactNode | β
| Content to show when role matches |
Usage Examples
// Simple conditional
<BucklerGuard showIf={isPremium}>
<PremiumFeature />
</BucklerGuard>
// With fallback
<BucklerGuard showIf={isPremium} fallback={<UpgradePrompt />}>
<PremiumFeature />
</BucklerGuard>
// RBAC mode
<BucklerGuard RBAC showForRole="admin" userRoles={['user', 'editor']}>
<AdminPanel /> {/* Won't show - user doesn't have 'admin' role */}
</BucklerGuard>
// RBAC with fallback
<BucklerGuard
RBAC
showForRole="admin"
userRoles={['admin']}
fallback={<AccessDenied />}
>
<AdminPanel /> {/* Will show - user has 'admin' role */}
</BucklerGuard>π― Route Patterns
Next-Buckler supports all Next.js routing patterns:
const routes = [
'/dashboard', // Static route
'/users/[id]', // Dynamic route
'/posts/[...slug]', // Catch-all route
'/shop/[[...slug]]', // Optional catch-all
'/api/[version]/users', // Multiple dynamic segments
]Pattern Matching Examples
| Route Pattern | Matches | Doesn't Match |
|--------------|---------|---------------|
| /users/[id] | /users/123, /users/abc | /users, /users/123/edit |
| /posts/[...slug] | /posts/a, /posts/a/b/c | /posts (requires at least one segment) |
| /shop/[[...slug]] | /shop, /shop/a, /shop/a/b | /other |
π‘οΈ Security Features
Path Traversal Protection
Next-Buckler automatically normalizes and validates paths:
// These attacks are prevented:
'/public/../admin' // Normalized to /admin (blocked if admin is private)
'//admin' // Normalized to /admin
'/./admin' // Normalized to /adminExact Route Matching
Dynamic routes use exact matching to prevent bypasses:
// Route: /users/[id]
'/users/123' // β
Match
'/users/123/admin' // β No match (prevents privilege escalation)
'/users' // β No matchRole Validation
// In strictMode, invalid roles throw errors
<Buckler
RBAC={{
admin: { grantedRoutes: ['/admin'] }
}}
userRoles={['superadmin']} // β Role doesn't exist
strictMode={true} // Will throw error
/>π Naming Conventions
Recommended Route Naming
// β
Good: Clear and semantic
const PRIVATE_ROUTES = [
'/dashboard',
'/profile',
'/settings',
'/admin',
'/admin/users',
]
// β Avoid: Inconsistent naming
const routes = ['/dash', '/user-profile', '/admin_panel']Role Naming Best Practices
// β
Good: Descriptive role names
const RBAC = {
'super-admin': { ... },
'admin': { ... },
'content-editor': { ... },
'viewer': { ... },
}
// β Avoid: Cryptic or inconsistent
const RBAC = {
'sa': { ... },
'Admin': { ... }, // Inconsistent casing
'content_editor': { ... }, // Mixed separators
}Route Groups Organization
// β
Good: Organized by feature
const ADMIN_ROUTES = ['/admin', '/admin/users', '/admin/settings']
const EDITOR_ROUTES = ['/editor', '/editor/posts', '/editor/media']
const USER_ROUTES = ['/dashboard', '/profile', '/settings']
const PRIVATE_ROUTES = [
...ADMIN_ROUTES,
...EDITOR_ROUTES,
...USER_ROUTES,
]π§ Advanced Utilities
Next-Buckler exports powerful internal utilities for advanced use cases.
verifyPath() - Path Validation & Security
Validates and normalizes paths with security checks.
import { verifyPath } from 'next-buckler'
const result = verifyPath('/users/[id]', '/users/123', ['user'])
// Returns:
{
valid: boolean, // true if path matches route pattern
normalizedPath: string, // Normalized path (removes .., //, etc.)
isDynamic: boolean, // true if route has dynamic segments
matched: boolean // true if path matches the route
}Security Features:
// Path traversal prevention
verifyPath('/admin', '/public/../admin', [])
// β { valid: true, normalizedPath: '/admin', ... }
// Double slash normalization
verifyPath('/admin', '//admin', [])
// β { valid: true, normalizedPath: '/admin', ... }
// Current directory removal
verifyPath('/admin', '/./admin', [])
// β { valid: true, normalizedPath: '/admin', ... }Dynamic Route Matching:
// Exact matching for security
verifyPath('/users/[id]', '/users/123', [])
// β { valid: true, matched: true, isDynamic: true }
verifyPath('/users/[id]', '/users/123/admin', [])
// β { valid: false, matched: false } // Prevents bypass!
// Catch-all routes
verifyPath('/blog/[...slug]', '/blog/a/b/c', [])
// β { valid: true, matched: true }
// Optional catch-all
verifyPath('/shop/[[...slug]]', '/shop', [])
// β { valid: true, matched: true }Use Cases:
- Custom route protection logic
- API route validation
- Middleware path checking
- Custom redirect logic
isDynamicRoute() - Route Pattern Detection
Check if a route contains dynamic segments.
import { isDynamicRoute } from 'next-buckler'
isDynamicRoute('/users/[id]') // β true
isDynamicRoute('/posts/[...slug]') // β true
isDynamicRoute('/shop/[[...slug]]') // β true
isDynamicRoute('/dashboard') // β false
// Use case: Different handling for dynamic routes
const routes = ['/users/[id]', '/dashboard', '/posts/[slug]']
const dynamicRoutes = routes.filter(isDynamicRoute)
const staticRoutes = routes.filter(r => !isDynamicRoute(r))getAccessRoute() - Determine User's Access Route
Get the appropriate route for a user based on their roles.
import { getAccessRoute } from 'next-buckler'
const RBAC = {
admin: {
accessRoute: '/admin',
grantedRoutes: ['/admin', '/dashboard']
},
user: {
accessRoute: '/dashboard',
grantedRoutes: ['/dashboard']
}
}
// User with admin role
getAccessRoute(['admin', 'user'], RBAC, '/dashboard')
// β '/admin' (first matching role's accessRoute)
// User with only user role
getAccessRoute(['user'], RBAC, '/dashboard')
// β '/dashboard'
// User with no matching roles
getAccessRoute(['guest'], RBAC, '/dashboard')
// β '/dashboard' (fallback)
// No authenticated user
getAccessRoute(undefined, RBAC, '/home')
// β '/home' (fallback)Use Cases:
- Custom login redirect logic
- Multi-tenant applications
- Role-based dashboard routing
- Post-authentication navigation
getGrantedRoutes() - Calculate User Permissions
Get all routes a user can access based on their roles.
import { getGrantedRoutes } from 'next-buckler'
const RBAC = {
admin: {
grantedRoutes: ['/admin', '/users', '/dashboard']
},
editor: {
grantedRoutes: ['/editor', '/dashboard']
}
}
// User with multiple roles gets union of all permissions
const routes = getGrantedRoutes(['admin', 'editor'], RBAC)
// β ['/admin', '/users', '/dashboard', '/editor']
// Duplicates are automatically removed
const routes2 = getGrantedRoutes(['user'], {
user: { grantedRoutes: ['/dashboard', '/profile', '/dashboard'] }
})
// β ['/dashboard', '/profile']
// Invalid roles trigger warnings in development
const routes3 = getGrantedRoutes(['invalid-role'], RBAC, true)
// β οΈ Logs: "[Buckler Security Warning] Role 'invalid-role' not found"
// β [] (empty array)Parameters:
function getGrantedRoutes(
userRoles: string[] | undefined,
RBAC: RoleAccess,
throwOnError?: boolean // strictMode (default: false)
): string[]Use Cases:
- Check if user can access a specific route
- Generate dynamic navigation based on permissions
- Implement custom authorization logic
- Build permission-aware UI components
Example: Custom Authorization Check
import { getGrantedRoutes } from 'next-buckler'
function useAuthorization() {
const { user } = useAuth()
const grantedRoutes = getGrantedRoutes(user?.roles, RBAC_CONFIG)
const canAccess = (path: string) => {
return grantedRoutes.some(route => {
// Handle dynamic routes
if (route.includes('[')) {
const pattern = route.replace(/\[.*?\]/g, '[^/]+')
return new RegExp(`^${pattern}$`).test(path)
}
return route === path
})
}
return { canAccess, grantedRoutes }
}
// In your component
function AdminButton() {
const { canAccess } = useAuthorization()
if (!canAccess('/admin')) {
return null
}
return <Link href="/admin">Go to Admin</Link>
}π οΈ Complete Advanced Example
Combining all utilities for a custom authorization system:
// lib/auth/permissions.ts
import {
verifyPath,
isDynamicRoute,
getAccessRoute,
getGrantedRoutes
} from 'next-buckler'
import { RBAC_CONFIG } from '@/config/rbac'
export class PermissionManager {
constructor(private userRoles: string[] | undefined) {}
/**
* Check if user can access a specific path
*/
canAccess(path: string): boolean {
const grantedRoutes = getGrantedRoutes(this.userRoles, RBAC_CONFIG)
return grantedRoutes.some(route => {
const { matched } = verifyPath(route, path, this.userRoles || [])
return matched
})
}
/**
* Get user's default landing page
*/
getDefaultRoute(fallback: string = '/dashboard'): string {
return getAccessRoute(this.userRoles, RBAC_CONFIG, fallback)
}
/**
* Get all accessible routes for UI generation
*/
getAccessibleRoutes(): string[] {
return getGrantedRoutes(this.userRoles, RBAC_CONFIG)
}
/**
* Filter menu items based on user permissions
*/
filterMenuItems<T extends { path: string }>(items: T[]): T[] {
return items.filter(item => this.canAccess(item.path))
}
/**
* Check if user has any of the required roles
*/
hasRole(...roles: string[]): boolean {
if (!this.userRoles) return false
return roles.some(role => this.userRoles!.includes(role))
}
/**
* Get role priority (useful for UI display)
*/
getPrimaryRole(): string | undefined {
const roleOrder = ['super-admin', 'admin', 'editor', 'user']
return roleOrder.find(role => this.userRoles?.includes(role))
}
}
// Usage in components
import { useAuth } from '@/hooks/useAuth'
import { PermissionManager } from '@/lib/auth/permissions'
export function usePermissions() {
const { user } = useAuth()
const permissions = new PermissionManager(user?.roles)
return permissions
}
// In your component
function FeatureComponent() {
const permissions = usePermissions()
if (!permissions.canAccess('/premium-feature')) {
return <UpgradePrompt />
}
return <PremiumFeature />
}π Performance Considerations
All utilities are optimized for production use:
// β
Route validation results are cached
verifyPath('/users/[id]', '/users/123', []) // Computed
verifyPath('/users/[id]', '/users/456', []) // Uses cached regex
// β
Granted routes are calculated once per role set
const granted1 = getGrantedRoutes(['admin'], RBAC)
const granted2 = getGrantedRoutes(['admin'], RBAC) // Same result
// β
Use memoization for expensive operations
import { useMemo } from 'react'
function MyComponent() {
const { userRoles } = useAuth()
const grantedRoutes = useMemo(
() => getGrantedRoutes(userRoles, RBAC_CONFIG),
[userRoles]
)
// grantedRoutes only recalculated when userRoles changes
}π¨ Best Practices
1. Define Routes as Constants
// β
Good: Type-safe and reusable
const PRIVATE_ROUTES = ['/dashboard', '/profile'] as const
const PUBLIC_ROUTES = ['/login', '/'] as const
export { PRIVATE_ROUTES, PUBLIC_ROUTES }2. Use Strict Mode in Development
<Buckler
strictMode={process.env.NODE_ENV !== 'production'}
// Catches configuration errors early
/>3. Implement onUnauthorizedAccess
<Buckler
onUnauthorizedAccess={(info) => {
// Log to analytics
analytics.track('unauthorized_access', info)
// Alert on repeated attempts
if (isRepeatedAttempt(info.path)) {
alertSecurityTeam(info)
}
}}
/>4. Use Hybrid Routes Wisely
// Good use case: Public content with auth-specific features
const HYBRID_ROUTES = [
'/blog', // Anyone can read
'/blog/[slug]', // Anyone can read
'/docs', // Anyone can read
]
// In the page, show extra features for authenticated users
<BucklerGuard showIf={isAuth} fallback={null}>
<BookmarkButton />
<CommentSection />
</BucklerGuard>5. Organize RBAC Configuration
// config/rbac.ts
export const RBAC_CONFIG = {
admin: {
accessRoute: '/admin',
grantedRoutes: ['...']
},
// ... other roles
} as const
// Use in _app.tsx
import { RBAC_CONFIG } from '@/config/rbac'6. Custom Loading Components
// components/AuthLoading.tsx
export function AuthLoading() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<Spinner className="h-12 w-12" />
<p className="mt-4 text-gray-600">Verifying authentication...</p>
</div>
</div>
)
}
// Use in Buckler
<Buckler LoadingComponent={<AuthLoading />} />7. Handle Multiple Roles
// User can have multiple roles
const userRoles = ['editor', 'moderator', 'premium']
// Buckler checks if ANY role has access
<Buckler
RBAC={{
editor: { grantedRoutes: ['/editor'] },
moderator: { grantedRoutes: ['/moderation'] },
premium: { grantedRoutes: ['/premium'] },
}}
userRoles={userRoles} // User gets access to all granted routes
/>π€ Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Development
# Clone the repository
git clone https://github.com/esaud17/next-buckler.git
# Install dependencies
npm install
# Run tests
npm test
# Build the library
npm run build
# Check bundle size
npm run sizeπ Project Stats
- Bundle Size: ~4KB (CJS) / ~4KB (ESM) - Well under 25KB limit β
- Test Coverage: 70+ tests across 6 suites β
- TypeScript: 100% type-safe β
- Dependencies: Zero runtime dependencies β
- Tree-Shakeable: Import only what you need β
π Motivation & Credits
After countless hours writing the same authentication boilerplate across projects, Next-Buckler was born from the frustration of:
- Copying auth logic between projects
- Managing complex redirect flows
- Preventing flash of unauthorized content
- Handling role-based permissions manually
- Writing repetitive
useEffecthooks for route protection - Keeping menu configurations in sync with route permissions
Next-Buckler solves all of this in a single, elegant solution. With v1.2.6's menu-based configuration, you can now eliminate an additional 150-200 lines of boilerplate per project.
π Credits
This library was inspired by NextShield (2021), built with π by @imjulianeral.
Next-Buckler extends and enhances these foundational ideas with menu-based RBAC configuration, advanced security features, framework integrations, and modern Next.js patterns.
Thank you, JuliΓ‘n! π
π Useful Links
- π¦ npm Package: next-buckler
- π GitHub Repository: esaud/next-buckler
- π Changelog: CHANGELOG.md
- π Report Issues: GitHub Issues
- π‘ Feature Requests: GitHub Discussions
π License
MIT Β© esaud.rivera
π Show Your Support
If Next-Buckler saves you time and makes your Next.js projects more secure:
- β Star this repo on GitHub
- π¦ Share it with the Next.js community
- π¬ Tell us how you're using it
- π€ Contribute improvements or new features
Every star motivates continued development! π
