next.jssingle-page-applicationnextjs-15nextjs-middleware

Secure SPA with dynamic loading using next/dynamic but with *security*


this works really well to do lazy loading to make a performant SPA:

import dynamic from 'next/dynamic';


const DynamicComponents: Record<string, React.ComponentType<ComponentProps<any>>> = {

  UserPushNotifUpdates: dynamic(() => import('@/components/dd/push-notif/push-notifs-component'), {
    loading: () => <LoadingComponent />,
  }),

  MatchPublic: dynamic(() => import('@/components/dd/match/match-2'), {
    loading: () => <LoadingComponent />,
  }),

  MatchPrivate: dynamic(() => import('@/components/dd/match/match'), {
    loading: () => <LoadingComponent />,
  }),

  AdminHome: dynamic(() => import('@/components/dd/admin/home/admin-home'), {
    loading: () => <LoadingComponent />,
  }),

  AdminDashboard: dynamic(() => import('@/components/dd/admin/dashboard/admin-dashboard'), {
    loading: () => <LoadingComponent />,
  }),

};

the problem is that for security anybody could load any component - I want to restrict access to components in the same way we do with pages.

To my best knowledge, with Next.js, these import calls bypass middleware.

is there a way to get the middleware to work so we can always intercept import calls to modules like this and check for access?


Solution

  • alright so this is pretty straightforward generally, sadly only some will understand it, the key part is to use update (backend) Next.js middleware to match on components/assets being loaded.

    this line:

    req.nextUrl?.pathname?.startsWith('_next/')
    

    the above will match on the components that get loaded on front-end, and there you can put your backend logic

    const privateRouteMatcher = createRouteMatcher([
      '/u/(.*)',
      '/api/p/(.*)',
    ]);
    
    
    export default middleware(async (auth, req: NextRequest) => {
     
    
      const res = NextResponse.next();
    
    
      if (req.nextUrl?.pathname?.startsWith('_next/')) {
    
        // HANDLE ACCESS CONTROL LOGIC HERE
        res.headers.set('x-middleware-cache', 'no-cache');
    
        return res;
      }
    
      // For non-private routes, return early with CORS headers
      if (!privateRouteMatcher(req)) {
        return res;
      }
    
      
       // route to rest of the app
      return res;
    });
    
    export const config = {
      matcher: [
        '/(.*)',
      ],
    };