Sometimes we need to allow different types of users to realize distinct actions depending on the profile type of that specific user. For example only allow administrators to delete specific objects.
First we need to create a new folder an initialize a node project
mkdir NodeMongoPermissions
cd NodeMongoPermissions
npm init -y
This is a list of all the packages needed and the reason why we will install them:
"cors" (npm install cors): CORS stands for Cross-origin resource sharing
is a mechanism that only allow access to our resources for specified domains
"dotenv-flow" (npm install dotenv-flow): Allows to load variables from .env files,
we can have multiple env files depending of the environment we are
using, for example development and production
"express" (npm install express): Simplifies the creation of RESTful APIs
using Node
"helmet" (npm install helmet): Increases express apps security by implementing
security http response headers`
"mongoose" (npm install mongoose): Object Document Mapping that helps creating
schemas, validations and queries when using Mongo
npm i cors dotenv-flow express helmet mongoose
Ceate a new file called .env in the base of the project, this file will contain our env variables.
# /.env
# Mongo
MONGO_URL=mongodb://localhost:27017/nodePermissions
Create a new folder called services in the base of the project, then create a new file called mongoose.js inside of this new folder. This file will initialize mongoose using the variable inside of our .env file.
// ./services/mongoose.js
const mongoose = require('mongoose');
// Initialize Mongoose
const startMongoose = async () => {
await mongoose.connect(process.env.MONGO_URL);
const db = mongoose.connection;
console.log("Mongo connected");
// handle mongoose error
db.on("error", async () => {
console.log('Mongoose error')
});
// handle mongoose disconnect
db.on("disconnected", async () => {
console.log('Mongoose disconnected')
});
}
module.exports = startMongoose;
Now we will create our express server, first create an app.js file
// ./app.js
require("dotenv-flow").config();
const express = require("express");
const helmet = require("helmet");
const cors = require("cors");
const startMongoose = require("./services/mongoose");
const permissionsRouter = require("./routes/permissions");
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(helmet());
app.use(cors());
/**
*
* @desc initialized all needed services
*/
const initializeConnections = async () => {
try {
await startMongoose();
} catch (e) {
console.log(e);
process.exit(1);
}
};
initializeConnections().then(() => {
app.get("/", (_req, res) => {
res.status(200).send('Working');
})
app.use(permissionsRouter)
});
module.exports = app;
now create a new file called index.js on the base of the project. We will use the app from above to listen for new requests
// ./index.js
const app = require("./app");
// Port that will be uses to listen to requests
const port = process.env.PORT || 3015;
/**
* Listen on port 3015
*/
app.listen(port, () => {
console.log(`App listening on port ${port}!`);
});
Now we will create our mongoose models, this models define how the data in the database is going to look like, its like a blueprint of all the objects in our database. Create a new folder called models in the base of your project. Inside of this new folder create a file called users.js this will contain all the available permissions un our platform, as you can see the default value is set to false, so we avoid allowing new permissions by mistake.
// ./models/users.js
const mongoose = require("mongoose");
// Users schema
const UsersSchema = new mongoose.Schema(
{
username: {
type: String,
},
/**
* Array with all the permissions, default value will false in case new
* permissions are added
*/
permissions: {
getData: { type: Boolean, default: false }
},
}
);
// Dont allow repeated usernames
UsersSchema.index({ username: 1 }, { unique: true });
// Create mongoose model and set the collection name to Users
const Users = mongoose.model("Users", UsersSchema, "Users");
// Export created model
module.exports = Users;
Next we will create the routes to execute specific logic when the user calls certain endpoints, first add a new folder called routes to the base of the project. Inside of this new folder create a file called permissions.js, here we will create three routes one for creating new users, another for updating the permissions of a user and last, one only accesible by users with the right permissions.
// ./routes/permissions.js
const express = require("express");
const permissionsController = require("../controllers/permissions");
const { permissionsMiddleware } = require("../middlewares/permissions");
// Express router
const router = express.Router();
/**
* @desc Will save a new user in the database
* @param {*} req express request
* @param {*} res espress response
*/
const saveUser = async (req, res) => {
try {
let username = req.body.username;
// If username is not present on the request body then the call is not valid
if (username) {
// Call controller logic
let ceateUserResponse = await permissionsController.saveUser(
username
);
// Return controller response
return res.status(200).send(ceateUserResponse);
}
// Return bad request error
res.status(400).send("Error data incomplete");
} catch (error) {
console.log(error);
// Return error message
res.status(500).send("Error saving new user");
}
};
/**
* @desc Will update the permissions of a user
* @param {*} req express request
* @param {*} res espress response
*/
const updateUserPermissions = async (req, res) => {
try {
let username = req.body.username;
let permissions = req.body.permissions;
// If username or permissions are not present on the request body then the call is not valid
if (username && permissions) {
// Call controller logic
let updatePermissionsResponse = await permissionsController.updateUserPermissions(
username,
permissions
);
// Return controller response
return res.status(200).send(updatePermissionsResponse);
}
// Return bad request error
res.status(400).send("Error data incomplete");
} catch (error) {
console.log(error);
// Return error message
res.status(500).send("Error updating permissions");
}
};
/**
* @desc Example of a route only accesible with required permissions
* @param {*} _req express request
* @param {*} res espress response
*/
const routeWithPermissions = async (_req, res) => {
res.status(200).send("Route with permissions");
};
// Route that saves a new user
router.post(
"/save-user",
saveUser
);
// Route that update a user permissions
router.post(
"/update-user-permissions",
updateUserPermissions
);
// Route only accesible with permissions, only accesible if the user has the required permissions
router.post(
"/route-with-permissions",
[permissionsMiddleware(["getData"])],
routeWithPermissions
);
module.exports = router;
Now we will add the controller logic, the controllers are responsable of running all the logic associated with a route and interacting with our models. Let's create a new folder called controllers in the base of our project, inside of this folder create a file called permissions.js.
// ./controllers/permissions.js
// Import users mongoose model
const UsersModel = require("../models/users");
const saveUser = async (username) => {
/**
* Creates new user with username from parameter, all permissions will be set to false
* because of the default value.
*/
const newUser = new UsersModel({
username
});
/**
* If the username includes the word admin then we will set all permissions to true,
* this is just to display we can change the permissions of the user we create
* deppending on specific logic
*/
if (username.includes('admin')) {
// Move through all the permissions available and set them to true
for (const key in newUser.permissions) {
newUser.permissions[key] = true;
}
}
// Save new user in Mongo
await newUser.save()
// Return saved user
return newUser
}
const updateUserPermissions = async (username, permissions) => {
// Update permissions from user with passed username
await UsersModel.updateOne(
{
username
},
{
$set: {
permissions
}
}
)
/**
* Obtain user information by executing query, we use lean to
* skip hydrating the result documents
*/
const userInformation = await UsersModel.findOne({ username }).lean().exec()
// Return user with updated information
return userInformation
}
module.exports = {
saveUser,
updateUserPermissions
}
Next we will create the middlewares, which are middle functions, in other words functions that will be called before other functions, in this case the router endpoint logic. We can specify these by calling one or an array of many before calling the function that will execute the logic of the route. Example:
router.post(
"/route-with-permissions",
permissionsMiddleware(["getData"]), // Single middleware function
routeWithPermissions
);
router.post(
"/route-with-permissions",
[permissionsMiddleware(["getData"])], // Array of middleware functions
routeWithPermissions
);
Let's continue with the project by adding a new folder called middlewares in the base of the project, now add a new file called permissions.js to this folder.
// ./middlewares/permissions.js
// Import users mongoose model
const UsersModel = require("../models/users");
/**
* @desc Checks if user has the right roles to access route
* @param {string[]} module the name of the module
*/
const permissionsMiddleware = (requiredPermissions) => async (req, res, next) => {
try {
/**
* Information needed to indentify user, in this case we will send the username in
* the boddy, but other information can be used for example the _id of the user
* obtained from decoding a JWT or a field in the request headers
*/
let username = req.body.username;
/**
* Obtain user information by executing query, we use lean to
* skip hydrating the result documents
*/
const userInformation = await UsersModel.findOne(
{
username
},
)
.lean()
.exec();
// Move through all the required permissions
for (let i = 0; i < requiredPermissions.length; i += 1) {
if (!userInformation.permissions[requiredPermissions[i]]) {
/**
* If the user does not have one of the required permissions
* return forbidden error
*/
return res.status(403).send("Not enough permissions");
}
}
// If the user has all the required permissions then continue
next();
} catch (error) {
// Return error message
return res.status(500).send(error);
}
};
module.exports = {
permissionsMiddleware
}
Add needed permissions to the user model, you can send this information to the Front End and display pages or components depending on the user permissions. Also remember to protect the routes that update the user permissions behing authentication. For example you could add a middleware that validates a JWT token and use the decoded information from the token to search for the user information needed in the permissions middleware.
This postman collection https://documenter.getpostman.com/view/11491178/2s946k5qB5 includes all the routes in the project, so you could test and see how the implementation works.
In this repository https://github.com/obravocedillo/NodeMongoPermissions you can find the code.