skip to Main Content

I am implementing an admin panel and don’t want to expose the panel’s front-end code to clients. I figured the best approach would be to configure npm run build to create two builds – one client build and one admin build. A and then the back-end would control which build gets returned based on authentication.

Possible duplicate with an answer, but doesn’t actually explain how you would do that if you are not already familiar with how the build process works inside out. Also, webpack Entry Points do look like something that would be applied here, but as someone who is not very familiar with webpack the limited non beginner-friendly documentation kinda goes over my head.

Some information on my setup:
I have and ReactJS / NodeJS SPA. Front-end and back-end are configured in monorepo principle where both share node_modules, package.json, .env, and so on. For that, I used react-app-rewired to change the path for npm run build and npm run start commands without the need to mess with webpack.

Here is my file structure:

back-end/
    ...
front-end/
    public/
    src/
        admin/ <- Would prefer the admin panel front-end to be here if possible
        ...
build/
    ...
build_admin/ <- This is what I want
    ...
node_modules/
    ...
.env
.gitignore
config-overrides.js
package.json
...

"scripts" from package.json:

"scripts": {
    "start": "node ./back-end/server.js",
    "build": "react-app-rewired build",
    "front-end": "set HTTPS=true&&set SSL_CRT_FILE=...&&set SSL_KEY_FILE=...&&react-app-rewired start",
    "back-end": "nodemon ./back-end/server.js",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },

So if my approach is practical – how do I set up npm run build to make two builds from select* src/ files?

*By select I mean for the client build ignore the admin/ source files and for admin build just build with admin/ files.

Some additional points to get ahead of alternative solutions:

  • I want to make the admin panel in React as a SPA so Node View Engine is not an option.
  • I don’t want to waste resources by spinning up a whole separate app just to run a basic admin panel and not to mention the headache of dealing with sharing data between two separate applications.
  • Reason, why I am avoiding showing the admin panel front-end code in the first place, is not that there will be hard-coded sensitive data, but because you can infer quite a lot of information based on UI(input fields, description, button names, graphs, etc).

2

Answers


  1. As of now, not an answer for your original question, how to make two different builds.

    A way to make it harder for people to look at the admin page source and easier for you to deploy, is to use the code splitting capabilities of Webpack.

    React provides a simple way to split your app in bundles, with the React.lazy method. However I think you need React 16.6.0 and above

    For webpack code splitting you’ll need version 4 and above

    I used react 18.2.0, webpack 5.75.0

    I bootstraped the app with npx create-react-app to go fast.

    I think react-app-rewired keeps default cra configuration and you should be good. Maybe you will need to update some webpack config, will see


    If you are using some kind of routing, you can inspire yourself from this code. I used react-dom-router 6.8 version

    The App.js file

    import { BrowserRouter, Routes, Route, Link,Navigate } from 'react-router-dom';
    import { ProtectedRoute } from './ProtectedRoute';
    import * as React from 'react'
    
    // The React.lazy() will normally handle everything for you
    const Admin = React.lazy(() => import('./AdminPage'))
    
    function App() {
      return (
        <BrowserRouter>
          <h1>Lazy Router</h1>
    
          <nav>
            <Link to="/">Home</Link>
            <Link to="/admin">Admin page</Link>
          </nav>
          <Routes>
            <Route path='/' element={<HomePage/>} />
            <Route path='/admin' element={
              <ProtectedRoute
              element={<Admin/>}
              fallback={<Navigate to="/" replace/>}
                />
            }
            />
    
          </Routes>
        </BrowserRouter>
      );
    }
    
    const HomePage = () => {
      return (
        <div>
          Home page
        </div>
      )
    }
    
    
    export default App;
    

    So you can pass the Component you want to protect, the AdminPanel Page into the ProtectedRoute component. Its goals is to call your API to check if it has permission to acces the AdminPanel.

    I defined two props, an element, any component you want, here the AdminPanel component wrapped with React Lazy.
    And a fallback, A Redirect to the HomePage if authorization failed.

    The ProtectedRoute.js

    import { useEffect, useState } from 'react';
    import * as React from 'react'
    const ProtectedRoute = (props) => {
    
        /* You could write your own hook that will performed the authorization call to your API. 
            The hook could returns load and authorized states
            But for simplicity
        */
        // Auhtorization state which reflects your api auth
        const [authorized, setAuthorized] = useState(false)
        // Load state, just to wait until isAuth function returns
        const [load, setLoad] = useState(false)
    
    
        // An inelegant function to mimic authorization call to the server
        const isAuth = () => {
            setTimeout(() => {
                setAuthorized(true)
                setLoad(true)
            }, 2000)
        }
    
        // Call your API when the ProtectedRoute component is mounted
        useEffect(() => {
            isAuth()
        },[])
        // Loader until the authorization complete and eventually your AdminPanel is loaded
        return (
            <React.Suspense fallback={<>...loading</>}>
                {load ?
                    authorized ?
                        props.element
                        : props.fallback
                    : <>...loading</>}
            </React.Suspense>
        )
    
    }
    export { ProtectedRoute }
    

    And the AdminPage.js

    const AdminPage = () => {
      return (
        <div>
          Admin Page
        </div>
      )
    }
    export default AdminPage
    

    You will see that the chunk for the adminPanel will only be downloaded when you request it, i.e. when authorization is successful.

    Note, if the bundle fails to load with React.lazy, your react application will completely crash. You have to protect it with a <MyErrorBoundary></MyErrorBoundary> component.
    This is explained in this react documentation

    Is this method 100% bullet proof, can some people hack a bit react state ? I won’t be surprise if it is the case. But it still makes it harder for people to access the source code of your AdminPanel.

    In addition, if your react bundles are distributed by a server you control, when the client requests the bundle for the admin page, you can check its rights.

    By default, bundles are named with barbarian names, and will be hard to filter, but I think you can give them the name of the module in question, according to the webpack documentation, in the output:{filename:etc..} section. This would be much easier to filter and control, when a client request the adminpanel.bundle.js file.

    Will edit my response if you need some precisions and after playing a bit with webpack, but for now I am lazy

    Login or Signup to reply.
  2. You should definitely use webpack entry points. Config is not really difficult and I presume it can be settled with react-app-rewired (see Extended Configuration Options chapter).

    To avoid any overburden, the idea is to use a path as entry point name and the [name] joker for output name. In your case, you could try to start with this basic config that may be adapted to correctly resolve real entry paths :

    entry: {
       './build/index': './front-end/src/index.js',
       './build_admin/index': './front-end/src/admin/index.js'
    },
    output: {
        path: './',
        filename: '[name].js'
    }
    

    It will create a bundle at ./build/index.js from first entry point and a bundle at ./build_admin/index.js from second entry point. You can also provide more details to handle common dependencies and so on.

    the ./ may not resolve to your project root folder but you can tweak this as needed, with a starting ../ for instance.

    If needed, you may have to require path to correctly resolve paths.

    See this thread for more examples : How to set multiple file entry and output in project with webpack?

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search