MX User Auth Roles
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:
-
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. -
Middlewares that client can use to protect their app's Routes.
Pre-Requisites
Your app MUST have the following modules installed:
- Mongoose
- Express
- jsonwebtoken
- bcryptjs
- morgan
- dotenv
npm install mxuserauthroles mongoose express jsonwebtoken mongoose bcryptjs morgan dotenv
Your app also needs to support the following infrastructure:
-
JWT Authorization
Requests need to have a header with key-value of {"authorization" : "Bearer "}
-
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:
-
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, };
-
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", }, });
-
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" } ] }
-
A callback function that usually runs the Connect to Database code chunk.
-
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:
-
Client's Express App instance (app)
-
Client's app's
UserModel
-
Client's
JWT Secret
Optional parameters:
-
routeHandle
Your API route handle for the userRole controllers. Default: "/api/userRoles"
-
jwtIDKey
Your ID Prop used in your JWT's
createToken
. Default: "id", -
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
-
Import middlewares in your routes:
const { mustBeAdmin, mustBeSuperAdmin, isAllowedToPerformAction, setupRequireLoginMiddleware, isProfileOwner, } = require("mxuserauthroles");
-
Setup the requiredLogin method.
// setupRequireLogin const requireLogin = setupRequireLoginMiddleware( UserModel, process.env.JWT_SECRET );
-
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
-
Use Postman and Sign In as a SuperAdmin.
-
Grab the token.
-
Execute endpoint to Retrieve all actions below.
-
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)
-
GET to retrive all Actions:
GET
api/userRoles/actions
OR
GET
api/userRoles/actions?keyword=[somekeyword]
-
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", }
-
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.
-
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.
-
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)
-
GET to retrive all Types:
GET
api/userRoles/types
OR
GET
api/userRoles/types?keyword=[somekeyword]
-
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"], }
-
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"] }
-
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.
-
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 };