mxuserauthroles

    1.1.0 • Public • Published

    MX User Auth Roles

    Buy me a Coffee

    Example Implementation

    A Modular Library to Manage User Roles and their responsibilities. It is meant to be used as a modular plugin to your existing MongoDB-Mongoose-Express application. It provides:

    1. Endpoints to manage (view, create, update, delete) User Types (Roles) and User Actions. User Types (such as Admin, Tester, Maintainer, etc) has a list of allowedActions that specify whether or not they are allowed to perform. Client's User Model can then implement the UserType as a property on their app.

    2. Middlewares that client can use to protect their app's Routes.

    Pre-Requisites

    Your app MUST have the following modules installed:

    1. Mongoose
    2. Express
    3. jsonwebtoken
    4. bcryptjs
    5. morgan
    6. dotenv
    npm install mxuserauthroles mongoose express jsonwebtoken mongoose bcryptjs morgan dotenv

    Your app also needs to support the following infrastructure:

    1. JWT Authorization

      Requests need to have a header with key-value of {"authorization" : "Bearer "}

    2. Your app should also have a mechanism to create a token. In this method, take note on the identifier (ID) property. For example:

      const createToken = (id, name, email) => {
        return jwt.sign(
          {
            id,
            name,
            email,
          },
          process.env.JWT_SECRET,
          {
            expiresIn: "30d",
          }
        );
      };

      In this case, the identifier prop is id. It will be used when setting up connectRoutesAndUserModel below, as it takes an optional parameter jwtIDKey

    Setup

    Import initialiseUserAuthRoles and connectRoutesAndUserModel in your server.js.

    const {
      initialiseUserAuthRoles,
      connectRoutesAndUserModel,
    } = require("mxuserauthroles");

    initialiseUserAuthRoles

    initialiseUserAuthRoles must accept FOUR mandatory parameters and ONE optional:

    1. Super Admin user properties. This needs to follow your UserModel's requirements. We recommend putting the credentials in .env file. Example:

      SUPER_ADMIN_ID=root-user-1
      SUPER_ADMIN_NAME=Application Super Admin
      SUPER_ADMIN_PASSWORD=root-user-1

      IMPORTANT: make sure the primary key/prop of your user model is defined FIRST

      Sample definition:

      const superAdminObject = {
        email: process.env.SUPER_ADMIN_ID, // make sure the primary key/prop of your user model is defined FIRST
        name: process.env.SUPER_ADMIN_NAME,
        password: process.env.SUPER_ADMIN_PASSWORD,
      };
    2. Your app's mongoose UserModel.

      NOTE: You user model must have a userType property, which is a Mongoose ID Reference.

      Sample Client User Model:

      const UserSchema = mongoose.Schema({
        email: {
          type: String,
          required: true,
          unique: true,
        },
        password: {
          type: String,
          required: true,
        },
        // IMPORTANT: Your User Model MUST HAVE THIS PROPERTY!
        userType: {
          type: mongoose.Schema.Types.ObjectId,
          ref: "UserType",
        },
      });
    3. The list of all default userActions. These can be modified later by superAdmin. One suggestion is to put them in a .json file which is later referenced in serves.js

      Sample json:

      {
        "actions": [
          {
            "name": "createProduct",
            "description": "Create a new product"
          },
          {
            "name": "placeOrder",
            "description": "Place an order and buy items"
          }
        ]
      }
    4. A callback function that usually runs the Connect to Database code chunk.

    5. An Optional EventEmitter object, that is used to inform top-level application that initialisation is ready. If you do decide to pass an event parameter, you may listen to the event initializationDone

      const events = require("events");
      const EM = new events.EventEmitter();
      
      const server = http.createServer(app);
      const PORT = process.env.PORT || 5000;
      module.exports =
        process.env.NODE_ENV === "test"
          ? server.listen(
              PORT,
              console.log(
                `Server running in ${process.env.NODE_ENV} mode on port ${PORT}`
              )
            )
          : // wait for the database is loaded before starting listening
            EM.on("initializationDone", () => {
              server.listen(PORT, () => {
                console.log(
                  `Server running in ${process.env.NODE_ENV} mode on port ${PORT}`
                );
              });
            });

    connectRoutesAndUserModel

    connectRoutesAndUserModel takes three mandatory parameters:

    1. Client's Express App instance (app)

    2. Client's app's UserModel

    3. Client's JWT Secret

    Optional parameters:

    1. routeHandle

      Your API route handle for the userRole controllers. Default: "/api/userRoles"

    2. jwtIDKey

      Your ID Prop used in your JWT's createToken. Default: "id",

    3. userPasswordProp

      The property for User Password fo your User Model. Default = "password"


    Sample initialisation code (server.js):

    // import
    const {
      initialiseUserAuthRoles,
      connectRoutesAndUserModel,
    } = require("mxuserauthroles");
    const UserModel = require("./models/User");
    const defaultUserActions = require("./defaultUserActions.json");
    
    // event emitter
    const events = require("events");
    const EM = new events.EventEmitter();
    
    // express
    const app = express();
    
    // body parser
    app.use(express.json());
    
    // CLIENT's user routes setup
    const UserRoutes = require("./routes/userRoutes");
    app.use("/api/users", UserRoutes);
    // ... every other routes ...
    
    // initialising library and connecting routes
    const superAdminObject = {
      email: process.env.SUPER_ADMIN_ID, // make sure the primary key/prop of your user model is defined FIRST
      email: process.env.SUPER_ADMIN_PASSWORD,
      password: process.env.SUPER_ADMIN_NAME,
    };
    initialiseUserAuthRoles(
      superAdminObject,
      UserModel,
      defaultUserActions,
      () => {
        initializeDatabase(process.env.NODE_ENV);
      },
      EM
    );
    connectRoutesAndUserModel(app, UserModel, process.env.JWT_SECRET);

    Middlewares

    The library comes with the following middlewares:

    • setupRequireLoginMiddleware:

      this to connect Client's User Model and JWT Secret to the Library. Example:

      const requireLogin = setupRequireLoginMiddleware(
        UserModel,
        process.env.JWT_SECRET
      );
      router.route("/").get(requireLogin, mustBeAdmin, getUsers);

      It requres 2 mandatory arguments:

      • Client's Mongoose User Model
      • JWT Secret

      There are another 2 optional arguments:

      • jwtIDKey: Your ID Prop used in your JWT's createToken. Default: "id",

      • userPasswordProp: The property for User Password fo your User Model. Default = "password"

    • mustBeAdmin & mustBeSuperAdmin:

      Endpoints exposed only to admin and superAdmin. These must be used after requireLogin

    • isProfileOwner:

      Used if there is an update user profile endpoint, to check if the request sender is either the Owner of the profile, or an admin/superAdmin

    • isAllowedToPerformAction:

      Check the user's allowedActions to see if the current endpoint's action is allowed for the user to perform.

      It takes in a Action String as argument.

    Sample Usage with Middlewares to Protect Routes

    1. Import middlewares in your routes:

      const {
        mustBeAdmin,
        mustBeSuperAdmin,
        isAllowedToPerformAction,
        setupRequireLoginMiddleware,
        isProfileOwner,
      } = require("mxuserauthroles");
    2. Setup the requiredLogin method.

      // setupRequireLogin
      const requireLogin = setupRequireLoginMiddleware(
        UserModel,
        process.env.JWT_SECRET
      );
    3. These middlewares can be used in your routes:

    • the set up requiredLogin in (1)
    • mustBeAdmin
    • mustBeSuperAdmin
    • isAllowedToPerformAction
    • isProfileOwner

    Example Usage with Client's User Routes

    const {
      mustBeAdmin,
      mustBeSuperAdmin,
      isAllowedToPerformAction,
      setupRequireLoginMiddleware,
      isProfileOwner,
    } = require("mxuserauthroles");
    
    const requireLogin = setupRequireLoginMiddleware(
      UserModel,
      process.env.JWT_SECRET
    );
    router.route("/").get(requireLogin, mustBeAdmin, getUsers);
    router
      .route("/:id")
      .put(
        requireLogin,
        isAllowedToPerformAction("updateUserProfile"),
        isProfileOwner,
        updateUserProfile
      )
      .delete(requireLogin, isAllowedToPerformAction("deleteUser"), deleteUser);
    router.post("/login", signIn);
    
    module.exports = router;

    API Routes

    This library gives the Client API's out of the box to manage User Types(Roles), and User Actions. It also gives an endpoint for UI to check if a user is allowed perform a list of actions.

    For this reason, IT IS CRUCIAL to HAVE A SINGLE POINT OF REFERENCE of ALL THE AVAILABLE ACTIONS of the APP.

    Depending on your API Route handle configured above, we will use the default api/userRoles for these examples.

    Retrieving All Actions For Reference for Developers

    1. Use Postman and Sign In as a SuperAdmin.

    2. Grab the token.

    3. Execute endpoint to Retrieve all actions below.

    4. The defaultActions.json will only be used once, when the app first initialises. To manage actions and types after that, you need to use the endpoints.

    User Actions (only for superAdmins)

    1. GET to retrive all Actions:

      GET api/userRoles/actions

      OR

      GET api/userRoles/actions?keyword=[somekeyword]

    2. POST to create new Action:

      POST api/userRoles/actions

      Pass in the Request Body a UserAction object. Schema:

      {
        name: {
          type: String,
          required: true,
        },
        description: {
          type: String,
        },
        nonDeletable: {
          type: Boolean,
          default: false,
        }
      }

      Example request body:

      {
         name: "newAction",
         description: "Some new action",
      }
    3. PUT to edit Action:

      PUT api/userRoles/actions/:id

      Pass in the Request Body a UserAction object, the props to be updated. Example:

      {
        name: "updatedActionName";
      }

      NOTE: If you update an action's name, all userTypes referencing the the updated action will be updated as well to reflect the updated action.

    4. DELETE to remove an Action:

      DELETE api/userRoles/actions/:id

      NOTE: If you delete an action, all UserTypes referencing the deleted action will be updated to exclude that action.

    5. DELETE MANY to remove Actions:

      DELETE api/userRoles/actions/deleteMany

      Pass in the Request Body a list/array of actionIds:

      {
        actionIds: ["123", "234", "456"],
      }

      NOTE: If you delete an action, all UserTypes referencing the deleted action will be updated to exclude that action.

    User Types (only for superAdmins and admins)

    1. GET to retrive all Types:

      GET api/userRoles/types

      OR

      GET api/userRoles/types?keyword=[somekeyword]

    2. POST to create new Types:

      POST api/userRoles/types

      Pass in the Request Body a UserAction object. To add allowedActions during creation of User Type, you need to pass a list/array of Action Object IDs. Schema:

      name: {
       type: String,
       required: true,
      },
      description: {
       type: String,
      },
      allowedActions: [
       {
         type: String, // must be valid Mongoose ID's. Endpoint will get the name of the Action and push in this array.
       },
      ],
      nonDeletable: {
       type: Boolean,
       default: false,
      }

      Example request body:

      {
         name: "newTypeName",
         description: "Some new type",
         allowedActions: ["123", "234", "456"],
      }
    3. PUT to edit Types:

      PUT api/userRoles/types/:id

      Pass in the Request Body a UserType object, the props to be updated. To update allowedActions, you need to pass in the id's of Actions. Example:

      {
        name: "updatedTypeName",
        allowedActions: ["123", "234", "456"]
      }
    4. DELETE to remove an Types:

      DELETE api/userRoles/types/:id

      NOTE: If you delete a userType, all Users referencing the the deleted userType will fallback to type "Generic" which is non-Deletable.

    5. DELETE MANY to remove Actions:

      DELETE api/userRoles/types/deleteMany

      Pass in the Request Body a list/array of typeIds:

      {
        typeIds: ["123", "234", "456"],
      }

      NOTE: If you delete a userType, all Users referencing the the deleted userType will fallback to type "Generic" which is non-Deletable.

    Verify User is Allowed to Perform Action

    This endpoint can be used by Client UI / Front End to obtain user authorisation based specific UI components.

    GET api/userRoles/verify

    Pass in the Request Body a list/array of actions String. Example:

    {
      actions: [
          "deleteProduct",
          "updateProduct",
          "viewProduct",
          "updateUserProfile",
          "deleteUser",
        ],
    }

    You will expect to receive the following response:

    {
      deleteProduct: true,
      updateProduct: true,
      viewProduct: false,
      updateUserProfile: false,
      deleteUser: false
    }

    This will depend on UserType of the User making the request.


    Caveats

    Be sure that the Action String names are the same (same spelling, casing, etc) for both server and front end.

    Error Handling

    The middlewares will execute next(error) when there is an error or when authentication/authorization fails. You need to use Express error handlers to properly catch these errors and return them as response.

    Example:

    In server.js:

    app.use(notFound);
    app.use(errorHandler);

    Error Handlers:

    const notFound = (req, res, next) => {
      const error = new Error(`Not Found - ${req.originalUrl}`);
      res.status(404);
      next(error);
    };
    
    const errorHandler = (err, req, res, next) => {
      const errorCode = res.statusCode === 200 ? 500 : res.statusCode;
      res.status(errorCode);
      const errorResponse = {
        code: errorCode,
        message: err.message,
        stack: process.env.NODE_ENV === "production" ? null : err.stack,
      };
      res.json(errorResponse);
    };
    
    module.exports = { notFound, errorHandler };

    Install

    npm i mxuserauthroles

    DownloadsWeekly Downloads

    1

    Version

    1.1.0

    License

    MIT

    Unpacked Size

    121 kB

    Total Files

    28

    Last publish

    Collaborators

    • asyrul21