UNPKG

@tradecrush/next-route-guard

Version:

Convention-based route authentication middleware for Next.js applications

1 lines 27.5 kB
{"version":3,"sources":["../src/route-guard.ts","../src/index.ts"],"sourcesContent":["/**\n * Core implementation of Next Route Guard middleware.\n * This module contains the runtime logic for checking if a route should be protected\n * and enforcing authentication based on the route map generated at build time.\n */\nimport { type NextRequest, NextResponse } from \"next/server\";\nimport type { RouteGuardOptions, RouteMap } from \"./types\";\n\n/**\n * Default route to redirect to when authentication fails\n */\nconst DEFAULT_LOGIN_ROUTE = '/login';\n\n/**\n * Node in the route trie representing a route segment\n * Used for efficient route matching and protection status lookup\n */\ninterface RouteNode {\n // Whether this route is protected\n isProtected?: boolean;\n\n // Regular children by segment name\n children: Map<string, RouteNode>;\n\n // Dynamic child node (for [param] segments)\n dynamicChild?: RouteNode;\n\n // Catch-all child (for [...slug] or [[...slug]])\n catchAllChild?: {\n node: RouteNode;\n isOptional: boolean;\n };\n}\n\n/**\n * Creates a Next.js middleware function that enforces route authentication\n * based on the directory structure conventions in the app router.\n *\n * This is the main entry point for the runtime middleware that checks if a user\n * is authenticated and handles redirection for protected routes.\n *\n * @param options - Configuration options for the middleware\n * @returns A Next.js middleware function\n */\nexport function createRouteGuardMiddleware(options: RouteGuardOptions) {\n // Set up default options\n const {\n isAuthenticated,\n onUnauthenticated = (request) => {\n // Default behavior: redirect to login with return URL\n const url = request.nextUrl.clone();\n url.pathname = DEFAULT_LOGIN_ROUTE;\n url.searchParams.set('from', request.nextUrl.pathname);\n return NextResponse.redirect(url);\n },\n routeMap,\n defaultProtected = true,\n excludeUrls = ['/api/(.*)']\n } = options;\n\n // Build the route trie at initialization time for efficient matching\n const routeTrie = buildRouteTrie(routeMap);\n\n // Return the middleware function that will be executed for each request\n return async function routeGuardMiddleware(request: NextRequest) {\n const pathname = request.nextUrl.pathname;\n\n // Skip authentication check for excluded URL patterns (e.g., API routes)\n for (const pattern of excludeUrls) {\n const isRegex = pattern instanceof RegExp;\n // Convert string patterns to regex if needed\n const regex = isRegex ? pattern : new RegExp(`^${pattern.replace(/\\*/g, '.*')}$`);\n\n if (regex.test(pathname)) {\n // Excluded path - allow access without auth check\n return NextResponse.next();\n }\n }\n\n // Determine if the current route should be protected using the trie\n const isProtected = matchPath(pathname, routeTrie, defaultProtected);\n\n // If route is public, allow access without auth check\n if (!isProtected) {\n return NextResponse.next();\n }\n\n // For protected routes, check if the user is authenticated\n const isAuthed = await isAuthenticated(request);\n\n // If authenticated, allow access to the protected route\n if (isAuthed) {\n return NextResponse.next();\n }\n\n // User is not authenticated for a protected route, handle according to options\n return onUnauthenticated(request);\n };\n}\n\n/**\n * Builds a route trie from a route map for efficient path matching\n * This converts the flat route lists into a tree structure for O(k) lookups\n * where k is the depth of the path (number of segments).\n *\n * @param routeMap - Map of protected and public routes\n * @returns Root node of the route trie\n */\nfunction buildRouteTrie(routeMap: RouteMap): RouteNode {\n // Create the root node\n const root: RouteNode = {\n children: new Map()\n };\n\n // Add protected routes first\n for (const route of routeMap.protected) {\n addRouteToTrie(root, route, true);\n }\n\n // Add public routes (these will override protection status for the same paths)\n for (const route of routeMap.public) {\n addRouteToTrie(root, route, false);\n }\n\n return root;\n}\n\n/**\n * Adds a single route to the trie\n *\n * @param root - Root node of the trie\n * @param route - Route path to add\n * @param isProtected - Whether this route is protected\n */\nfunction addRouteToTrie(root: RouteNode, route: string, isProtected: boolean): void {\n // Split the path into segments and remove empty segments\n const segments = route.split('/').filter((segment) => segment !== '');\n\n let current = root;\n\n // Process each segment of the path\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i]!;\n\n // Handle catch-all routes: [...slug] or [[...slug]]\n if (segment.startsWith('[...') || segment.startsWith('[[...')) {\n const isOptional = segment.startsWith('[[...');\n\n // Create a catch-all node if it doesn't exist\n if (!current.catchAllChild) {\n current.catchAllChild = {\n node: { children: new Map() },\n isOptional\n };\n }\n\n // If this is the last segment, mark protection status\n if (i === segments.length - 1) {\n current.catchAllChild.node.isProtected = isProtected;\n }\n\n // Continue adding remaining segments even after a catch-all\n current = current.catchAllChild.node;\n }\n // Handle dynamic segments: [param]\n else if (segment.startsWith('[') && segment.endsWith(']')) {\n // Create a dynamic node if it doesn't exist\n if (!current.dynamicChild) {\n current.dynamicChild = { children: new Map() };\n }\n\n // If this is the last segment, mark protection status\n if (i === segments.length - 1) {\n current.dynamicChild.isProtected = isProtected;\n }\n\n // Move to the dynamic child for next iteration\n current = current.dynamicChild;\n }\n // Handle regular segments\n else {\n // Create a regular child if it doesn't exist\n if (!current.children.has(segment)) {\n current.children.set(segment, { children: new Map() });\n }\n\n // If this is the last segment, mark protection status\n if (i === segments.length - 1) {\n const child = current.children.get(segment)!;\n child.isProtected = isProtected;\n }\n\n // Move to the child for next iteration\n current = current.children.get(segment)!;\n }\n }\n\n // For root routes like '/' that don't have segments\n if (segments.length === 0) {\n root.isProtected = isProtected;\n }\n}\n\n/**\n * Improved visualization function for route tries with correct indentation\n *\n * @param trie - The route trie to visualize\n * @param options - Visualization options\n * @returns A string representation of the trie\n */\nfunction visualizeTrie(\n trie: RouteNode,\n options: {\n indent?: string;\n path?: string;\n } = {}\n): string {\n const { indent = '', path = '' } = options;\n\n let output = '';\n const protectionStatus =\n trie.isProtected !== undefined ? (trie.isProtected ? '🔒 Protected' : '🔓 Public') : '❓ Default';\n\n // Root node special case\n if (indent === '') {\n output += `Root (${protectionStatus})\\n`;\n }\n\n // Convert Map entries to array for easier handling\n const children = Array.from(trie.children.entries());\n\n // Process regular children\n children.forEach(([segment, childNode], index) => {\n const isLastChild = !trie.dynamicChild && !trie.catchAllChild && index === children.length - 1;\n const childPath = path + '/' + segment;\n const childStatus =\n childNode.isProtected !== undefined ? (childNode.isProtected ? '🔒 Protected' : '🔓 Public') : '❓ Default';\n\n // Use different connectors based on position\n const connector = isLastChild ? '└── ' : '├── ';\n output += `${indent}${connector}${segment} (${childStatus})\\n`;\n\n // Child indentation changes based on whether this is the last child\n const childIndent = indent + (isLastChild ? ' ' : '│ ');\n\n // Recurse into child\n output += visualizeTrie(childNode, {\n indent: childIndent,\n path: childPath\n });\n });\n\n // Process dynamic parameter child\n if (trie.dynamicChild) {\n const isLastChild = !trie.catchAllChild;\n const childStatus =\n trie.dynamicChild.isProtected !== undefined\n ? trie.dynamicChild.isProtected\n ? '🔒 Protected'\n : '🔓 Public'\n : '❓ Default';\n\n // Use different connectors based on position\n const connector = isLastChild ? '└── ' : '├── ';\n output += `${indent}${connector}[param] (${childStatus})\\n`;\n\n // Child indentation changes based on whether this is the last child\n const childIndent = indent + (isLastChild ? ' ' : '│ ');\n\n // Recurse into dynamic child\n output += visualizeTrie(trie.dynamicChild, {\n indent: childIndent,\n path: path + '/[param]'\n });\n }\n\n // Process catch-all child (always the last child if it exists)\n if (trie.catchAllChild) {\n const catchAllType = trie.catchAllChild.isOptional ? '[[...slug]]' : '[...slug]';\n const optionalText = trie.catchAllChild.isOptional ? ' (optional)' : '';\n const childStatus =\n trie.catchAllChild.node.isProtected !== undefined\n ? trie.catchAllChild.node.isProtected\n ? '🔒 Protected'\n : '🔓 Public'\n : '❓ Default';\n\n output += `${indent}└── ${catchAllType}${optionalText} (${childStatus})\\n`;\n\n // Catch-all is always the last child, so no need for vertical lines in indentation\n const childIndent = indent + ' ';\n\n // Recurse into catch-all child\n output += visualizeTrie(trie.catchAllChild.node, {\n indent: childIndent,\n path: path + '/' + catchAllType\n });\n }\n\n return output;\n}\n\n/**\n * Match a path against the route trie to determine if it's protected\n *\n * @param path - URL path to check\n * @param routeTrie - Route trie for efficient matching\n * @param defaultProtected - Default protection status\n * @returns true if the path should be protected, false if it's public\n */\nfunction matchPath(path: string, routeTrie: RouteNode, defaultProtected: boolean): boolean {\n // Clean and normalize the path\n let cleanPath = (path.split('?')[0] || '').split('#')[0] || '';\n if (cleanPath.endsWith('/') && cleanPath.length > 1) {\n cleanPath = cleanPath.slice(0, -1);\n }\n\n // Special case for root path\n if (cleanPath === '/') {\n return routeTrie.isProtected ?? defaultProtected;\n }\n\n // Split path into segments\n const segments = cleanPath.split('/').filter(Boolean);\n\n // Use recursive matching with backtracking\n return findMatch(routeTrie, segments, 0, defaultProtected);\n}\n\n/**\n * Recursively find the best match for segments in the route trie\n *\n * @param node - Current node in the trie\n * @param segments - Path segments\n * @param index - Current segment index\n * @param defaultProtected - Default protection status\n * @returns The protection status for the best matched path\n */\nfunction findMatch(node: RouteNode, segments: string[], index: number, defaultProtected: boolean): boolean {\n // If we reached the end of the path, return the protection status of this node\n if (index >= segments.length) {\n // If this node has an explicit protection status, use it\n if (node.isProtected !== undefined) {\n return node.isProtected;\n }\n // If this node has an optional catch-all child, use its protection status\n else if (node.catchAllChild && node.catchAllChild.isOptional) {\n return node.catchAllChild.node.isProtected ?? defaultProtected;\n }\n // Otherwise, use the default\n else {\n return defaultProtected;\n }\n }\n\n // Get the current segment\n const segment = segments[index]!;\n\n // Check for exact match in children\n if (node.children.has(segment)) {\n // Continue matching with the next segment\n const childNode = node.children.get(segment)!;\n return findMatch(childNode, segments, index + 1, defaultProtected);\n }\n // Check for dynamic parameter match\n else if (node.dynamicChild) {\n return findMatch(node.dynamicChild, segments, index + 1, defaultProtected);\n }\n // Check for catch-all match\n else if (node.catchAllChild) {\n // Handle rest segments after catch-all (if any)\n if (node.catchAllChild.node && node.catchAllChild.node.children.size > 0) {\n // Try to find a matching rest segment for the remaining path\n // We need to check all possible places where the catch-all might end\n\n // First, let's try the most specific match: check if any segments after the catch-all\n // match the remainder of our path\n const tryRestSegmentMatch = (startIndex: number): boolean | null => {\n // Make sure we have segments remaining\n if (startIndex >= segments.length) {\n return null;\n }\n\n const remainingSegment = segments[startIndex]!;\n\n // If we have a match in the rest segments, follow that path\n if (node.catchAllChild?.node.children?.has(remainingSegment)) {\n const restNode = node.catchAllChild.node.children.get(remainingSegment)!;\n return findMatch(restNode, segments, startIndex + 1, defaultProtected);\n }\n\n return null;\n };\n\n // Try each possible ending position for the catch-all\n for (let i = index; i < segments.length; i++) {\n const result = tryRestSegmentMatch(i);\n if (result !== null) {\n return result;\n }\n }\n }\n\n // If no rest segments matched or if there are no rest segments,\n // use the catch-all node's protection status\n return node.catchAllChild.node.isProtected ?? defaultProtected;\n }\n\n // No match found, use default protection status\n return defaultProtected;\n}\n","/**\n * Next Route Guard - Convention-based route authentication middleware for Next.js\n *\n * This package provides a simple way to protect routes in Next.js applications\n * based on directory naming conventions. It uses Next.js Edge middleware to\n * perform authentication checks at runtime based on a route map generated\n * during build time.\n *\n * @packageDocumentation\n */\n\nexport { createRouteGuardMiddleware } from './route-guard';\nexport type { RouteGuardOptions, NextMiddleware, RouteMap } from './types';\n\nimport type { NextFetchEvent } from 'next/server';\n\n/**\n * A middleware function that takes a request and returns a response.\n * This is compatible with Next.js middleware system.\n */\nexport type Middleware = (\n request: import('next/server').NextRequest,\n event?: NextFetchEvent\n) => Promise<import('next/server').NextResponse | undefined> | import('next/server').NextResponse | undefined;\n\n/**\n * A middleware factory that wraps a middleware.\n * This is used to create reusable middleware components that can be chained together.\n */\nexport type MiddlewareFactory = (middleware: Middleware) => Middleware;\n\n/**\n * Chain multiple middleware factories together.\n *\n * This allows you to compose multiple middleware functions into a single middleware pipeline.\n * Each middleware in the chain can choose to call the next middleware or short-circuit the chain.\n *\n * @param functions - Array of middleware factories to chain together\n * @param index - Current index in the chain (used internally for recursion)\n * @returns A middleware function representing the entire chain\n *\n * @example\n * ```ts\n * // Create middleware factories\n * const withLogging: MiddlewareFactory = (next) => {\n * return (request) => {\n * console.log(`Request: ${request.method} ${request.url}`);\n * return next(request);\n * };\n * };\n *\n * const withAuth: MiddlewareFactory = (next) => {\n * return (request) => {\n * if (!isAuthenticated(request)) {\n * return NextResponse.redirect('/login');\n * }\n * return next(request);\n * };\n * };\n *\n * // Use the chain\n * export default function middleware(req: NextRequest, ev: NextFetchEvent) {\n * return chain([withLogging, withAuth])(req, ev);\n * }\n * ```\n */\nexport function chain(functions: MiddlewareFactory[], index = 0): Middleware {\n const current = functions[index];\n\n if (current) {\n // Get the next middleware in the chain\n const next = chain(functions, index + 1);\n\n // Apply the current middleware factory to the next middleware\n return current(next);\n }\n\n // Base case: we've reached the end of the chain\n // Return a middleware that just returns undefined (letting Next.js continue to the actual route)\n return () => undefined;\n}\n\n/**\n * Generate a route map based on the Next.js app directory structure.\n *\n * This function analyzes the directory structure to identify routes and their protection status.\n * It's used by the CLI tools to generate the route map at build time or during development.\n *\n * Routes are classified as protected or public based on their directory context:\n * - Routes inside a \"(public)\" directory group are marked as public\n * - Routes inside a \"(protected)\" directory group are marked as protected\n * - Routes inherit protection status from their parent directories\n * - Routes are protected by default if not explicitly marked\n *\n * @param appDir - Path to the Next.js app directory\n * @param publicPatterns - Array of directory name patterns that indicate public routes\n * @param protectedPatterns - Array of directory name patterns that indicate protected routes\n * @returns Object containing either the generated route map or an error message\n */\nexport function generateRouteMap(\n appDir: string,\n publicPatterns: string[] = ['(public)'],\n protectedPatterns: string[] = ['(protected)']\n): { error?: string; routeMap?: { public: string[]; protected: string[] } } {\n // Make sure we're running in a Node.js environment\n if (typeof process === 'undefined' || !process.env) {\n return { error: 'This function can only be used in a Node.js environment' };\n }\n\n try {\n // We need to dynamically import these modules since they're not available in Edge runtime\n // This function is only intended to be used during build time or development\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const fs = require('fs');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const path = require('path');\n\n // Initialize the route map\n const routeMap: { public: string[]; protected: string[] } = {\n public: [],\n protected: []\n };\n\n /**\n * Recursively scans the directory structure to identify routes\n *\n * @param dirPath - Current directory path being scanned\n * @param segments - URL segments collected so far (for constructing the route path)\n * @param groups - Route groups encountered in the current path\n */\n function scanDirectory(dirPath: string, segments: string[] = [], groups: string[] = []) {\n // Skip if directory doesn't exist\n if (!fs.existsSync(dirPath)) return;\n\n // Read directory contents\n const items = fs.readdirSync(dirPath);\n\n // Process each item in the directory\n for (const item of items) {\n const itemPath = path.join(dirPath, item);\n const stat = fs.statSync(itemPath);\n\n if (stat.isDirectory()) {\n // Skip special directories like node_modules\n if (item === 'node_modules' || item.startsWith('.')) continue;\n\n // Check if this is a route group (enclosed in parentheses)\n const isRouteGroup = item.startsWith('(') && item.endsWith(')');\n const newGroups = [...groups];\n const newSegments = [...segments];\n\n if (isRouteGroup) {\n // Route groups are organizational only and don't affect the URL path\n newGroups.push(item);\n } else {\n // Regular directories become part of the URL path\n newSegments.push(item);\n }\n\n // Continue scanning subdirectories\n scanDirectory(itemPath, newSegments, newGroups);\n } else if (\n stat.isFile() &&\n (item === 'page.js' || item === 'page.tsx' || item === 'page.jsx' || item === 'page.ts')\n ) {\n // Found a page file, which represents a route endpoint\n const route = '/' + segments.join('/');\n const routePath = route === '//' ? '/' : route;\n\n // Determine if the route is protected based on its group context\n // Default to protected unless explicitly marked as public\n let isProtected = true;\n\n // Check route groups to determine protection status\n // Process groups in reverse order to prioritize the innermost (most specific) group\n // This behavior was enhanced in v0.2.2 to allow nested groups to override parent groups\n // For example, (public)/docs/(protected)/admin would make /docs/admin protected\n // despite being in a public parent group\n for (let i = groups.length - 1; i >= 0; i--) {\n const group = groups[i];\n if (group && publicPatterns.includes(group)) {\n isProtected = false;\n break;\n } else if (group && protectedPatterns.includes(group)) {\n isProtected = true;\n break;\n }\n }\n\n // Add to the appropriate category in the route map\n if (isProtected) {\n routeMap.protected.push(routePath);\n } else {\n routeMap.public.push(routePath);\n }\n }\n }\n }\n\n // Start the directory scan from the app root\n scanDirectory(appDir);\n\n // Sort the routes for better readability and consistency\n routeMap.protected.sort();\n routeMap.public.sort();\n\n return { routeMap };\n } catch (error) {\n return { error: error instanceof Error ? error.message : String(error) };\n }\n}\n"],"mappings":"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","names":["_a","_b","scanDirectory"]}