User Roles and rules(permission) access in node.js - node.js

I created a node.js application (Bus-ticket-booking app). MongoDB is the database system I'm using. I haven't yet finished the front end. I'm doing API queries with Postman.
For authentication, I'm using JWT. Now I want to add roles and rules for users such as the app's administrator, supervisor, and normal user.
1 -> A user can have many roles assigned to them (admin, supervisor).
2 -> Permissions can be assigned to a role ( Create, Update, delete etc...).
As a result, a user can have one or more roles, and each role can have one or more permissions. A user can use APIs for which he has rights, such as creating data, deleting data, updating data, and so on.
Here is the user schema:
const userSchema = new mongoose.Schema({
firstname: {
type: String,
required: true,
},
lastname: {
type: String,
required: true,
},
email: {
type: String,
unique: true,
required: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error("Please provide the valid email address");
}
},
},
password: {
type: String,
required: true,
trim: true,
minLength: 8,
},
phone: {
type: Number,
required: true,
unique: true
},
tokens:[{
token: {
type: String,
required:true
}
}]
},{
timestamps:true
});
I'm new to it and have very little knowledge about it.
Is there anyone who can assist me?

If you just need a couple different roles,
I suggest you go with Sajawal Hassan's concept of simply adding a boolean field to determine user's access level.
However, if you are planning to create where there are multitude of roles to be added, and do not want field to be added for each role:
Add permissions array field to data model and create a permission list (to better organize) to the user data model
set up a middleware to add to your routers
set up groups to allow the user of your routers, and pass them through routers
1a. I suggest you create a list of roles within the user model file. Possibly a dictionary.
.../models/user.js
const ROLES = {
ADMIN: "ADMIN",
SUPERVISOR: "SUPERVISOR"
}
...
module.exports = mongoose.model('User', UserSchema);
module.exports.ROLES = ROLES;
1b. add a array field to Users models which will have the roles as permissions
.../user.js
const userSchema = new mongoose.Schema({
...,
permissions: [String],
...
});
Set up a middleware or in this case you already have auth; add functional capabilities to the function in which it will check its parameter or attach it to options (if you are checking for token, or other auth params)
.../auth.js
module.exports = function (options) {
...
// get user, validate token b4 here
user.permissions.forEach(permission => {
if (options.allowedGroup.indexOf(permission)){
// if authenticated
return next();
}
}
// could not authenticate at this point
return next(errorHandler) // throw a error or handle it way you want
}
3a. set up groups to determine which roles will have access to each router or a set of routers
.../routes/api_123.js
const mongoose = require('mongoose');
const User = mongoose.model('User');
const readGroup = [User.ROLES.SUPERVISOR, User.ROLES.ADMIN];
const writeGroup = [User.ROLES.ADMIN];
3b. pass the group you made as allowedGroup in param of middleware and set it up with a asyncHandler
...
const asyncHandler = require('express-async-handler');
...
router.get('/user/:id', auth({allowedGroup: readGroup}), asyncHandler(async(req, res, next) => {
... // your stuff here
res.status(200).json(data);
}))
router.post('/user/:id', auth({allowedGroup: writeGroup}), asyncHandler(async(req, res, next) => {
... // your stuff here
res.status(200).json(data);
}))

You should try to watch a full course on express and mongodb but you would have to add fields in the user schema that specifies if the user has permissions i.e admin: { type: booleen, default: false } then set the booleen to true if you want the user to be admin then create a route for something only admin sould be able to do lets say to delete a user so then in there check if the admin field in user schema is true. If so then user can delete otherwise throw err. it would be really helpful if you were to provide code snippets and other helpful links learn more here: https://stackoverflow.com/help/how-to-ask
edit:
Do keep in mind im using mongodb atlas for the code
Add an admin field (or any role that you want im gonna go with admin here)
so change
const userSchema = new mongoose.Schema({
firstname: {
type: String,
required: true,
},
lastname: {
type: String,
required: true,
},
email: {
type: String,
unique: true,
required: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error("Please provide the valid email address");
}
},
},
password: {
type: String,
required: true,
trim: true,
minLength: 8,
},
phone: {
type: Number,
required: true,
unique: true
},
tokens:[{
token: {
type: String,
required:true
}
}]
},{
timestamps:true
});
to this
const userSchema = new mongoose.Schema({
admin: {
type: Booleen,
default: false,
},
firstname: {
type: String,
required: true,
},
lastname: {
type: String,
required: true,
},
email: {
type: String,
unique: true,
required: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error("Please provide the valid email address");
}
},
},
password: {
type: String,
required: true,
trim: true,
minLength: 8,
},
phone: {
type: Number,
required: true,
unique: true
},
tokens:[{
token: {
type: String,
required:true
}
}]
},{
timestamps:true
});
I just added the admin field in the user schema
Then lets say you only want the admin to be able to delete users
for that you would have to create a route like this
router.delete("/delete/:id", async (req, res) => {
try {
// First find the user admin wants to delete
const user = await User.findById(req.params.id) // getting id from the id you put in url
// Make sure the user who wants to delete another user is an admin
if (user.admin) {
await user.deleteOne() // This deletes the user
} else {
res.status(403).json("You are not allowed to do this action!")
}
} catch (error) {
res.sendStatus(500);
}
});

Related

mongoose .save() doesn't work on a specific collection

so I'm trying to create a party with creator field with id of a user, and at the same time adding a party id to users parties using mongoose sessions. Here's the code of a request:
const createParty = async (req, res, next) => {
const {title, description, address, creator} = req.body;
const createdParty = new Party({
title,
description,
image: 'https://media-cdn.tripadvisor.com/media/photo-s/14/03/b3/4e/tlv.jpg',
address,
creator,
savedBy: []
});
let user;
try {
user = await User.findById(creator);
} catch (err) {
let error = new HttpError('Fetching user failed', 500);
return next(error);
}
if (!user) {
return next(new HttpError('Could not find user for providen id', 404));
}
try {
const sess = await mongoose.startSession();
sess.startTransaction();
await createdParty.save({ session: sess });
user.parties.push(createdParty);
console.log(user);
await user.save({ session: sess });
await sess.commitTransaction();
} catch (err) {
let error = new HttpError('Creating party failed', 500);
return next(error);
}
res.status(201).json({party: createdParty});
};
And my user and parties schemas:
const userSchema = new Schema({
username: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true, minlength: 6 },
image: { type: String, required: true },
parties: [{ type: mongoose.Types.ObjectId, required: true, ref: 'Party' }],
savedParties: [{ type: mongoose.Types.ObjectId, required: true, ref: 'Party' }]
});
const partySchema = new Schema({
title: { type: String, required: true },
description: { type: String, required: true },
image: { type: String, required: true },
address: { type: String, required: true },
creator: { type: mongoose.Types.ObjectId, required: true, ref: 'User' },
savedBy: [{ type: mongoose.Types.ObjectId, required: true, ref: 'User' }]
});
The problem is I can't save a user with new party id, only this line fails:
await user.save({ session: sess });. Tried to move this line to a separate try/catch, tried to add user.markModified('parties'); didn't help. Please help those who may know the solution.🙏🏻
UPDATE ON THE PROBLEM
So I did some testing and found out that if I delete everything from the database, and I'll create a user I will be able to add parties, and it'll work as it should. But if I'll create another user and afterward will try to add a party to one of the users it won't work.
when you session it won't create the collection if it doesn't exist and you need to do it manually in the data

checking if object id is in an array of object id

I am trying to do a check to see if a logged-in user's id req.user.id. is in an array of followers of the user being checked in req.params.id, bit for some reason it doesn't work.
router.get('/api/:id/isfollowing', auth, async (req, res) => {
if (req.params.id==req.user._id) {
return res.status(200).send({ "isfollowing": "Myself" })
}
try {
const followers = await Follow.find({
user: req.params.id
})
let followersArr = followers.map(follower=>{
return follower.followedBy
})
const yes = followersArr.includes(req.user._id)
// const yes = followersArr.filter((objId) => objId==req.user._id)
console.log(yes, followersArr, req.user._id)
if (yes===false) {
return res.status(200).send({ "isfollowing": false })
}
return res.status(200).send({ "isfollowing": true })
} catch (e) {
res.status(500).send()
}
})
for some reason the check doesn't work and even when using the filter, it still returns nothing. But when I console.log the values, it is right there.
[] [ 5fa4f0af4a7bf5471c41e225, 5f9dc1777a695570e878424d ] 5f9dc1777a695570e878424d
EDIT
schemas below
User schema
const userSchema = new mongoose.Schema({
fullname: {
type: String,
required: true,
trim: true,
lowercase: true
},
username: {
type: String,
unique: true,
required: true,
trim: true,
lowercase: true
},
email: {
type: String,
unique: true,
required: true,
trim: true,
lowercase: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error('Email is invalid')
}
}
},
password: {
type: String,
required: true,
minlength: 7,
trim: true,
validate(value) {
if (value.toLowerCase().includes('password')) {
throw new Error('Passwoed cannot contain "password"')
}
}
}
})
follow schema
const followSchema = new mongoose.Schema({
// the logged in user who will be trying to follow someone will be added to "followedBy"
// the user who is getting followed will be added to "user"
user: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
},
followedBy: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'Showcase'
}
}, {
timestamps: true
})
I gave follow its own schema so I can record other info like time and other info whenever a user follows another.
If I've understand well you only need a simple query.
Since you only want to know if the id is into an array, you can check that directly with mongo. You don't need load every document into memory and use JS functions like filter or something similar.
You only need a query similar to this:
db.collection.find({
"user": ObjectId("user_id"),
"followedBy": ObjectId("follower_id")
})
This will return a document that match both values.
Check here it works and tell me if is the behaviour and output you expect.
Also I will code a mongoose query and I'll update the answer.
You can use also this query in mongoose to get how many documents find the query:
var find = await model.find({"user":mongoose.Types.ObjectId(user_id),"followedBy":mongoose.Types.ObjectId(follower_id)}).countDocuments()
Includes cannot be used in this case since you are trying to find ObjectId in an array.
To find if req.user._id is present in followersArr, use Array.some() function as below
const yes = followersArr.some(followerId=>{followerId.equals(req.user._id)})
The some call will iterate over the followersArr array, calling equals on each one to see if it matches req.user._id and stop as soon as it finds a match. If it finds a match it returns true, otherwise false.
You can't use something simpler like indexOf because you want to compare the ObjectIDs by value, not by reference.

How to create MongoDB schema design while dealing with single User account and multiple user (Organization account with roles)

I am working on a Nodejs Express API project using mongoDB with mongoose and i would like to get some advice on best practices and going about creating an efficient schema design from community
The app deals with two type of user accounts
Account type :
Single (default)
Organization (can switch to from settings)
Note:
In organisation account there will be a admin (owner) and other invited user and each user is assigned permission level / access level .One user will always be associated with only one account, ie he cannot be invited again to another account or start a new account if he is already part of a existing account. Also billing and shipping address is specific to account rather than user in the case of an organization account (address of user switching to organization account will be the address of Organization account )
I have completed the authentication part with the help of passport.js JWT and local strategy
i tried to develop one similar to RDBMS approach ( i used to be RDBMS guy ) and failed
Models and schemas
const userSchema = new Schema({
first_name: String,
last_name: String,
email: String,
phone: String,
avatar: String,
password: String,
active: Boolean
});
const User = mongoose.model('user', userSchema);
const accountSchema = mongoose.Schema({
account_type: { type: String, enum: ['single', 'organization'], default: 'single' },
organization: { type: Schema.Types.ObjectId, ref: 'organization', required: false },
billing_address: String,
shipping_address: String,
});
const Account = mongoose.model('account', accountSchema);
const accountUserRoleSchema = mongoose.Schema({
user : { type: Schema.Types.ObjectId, ref: 'user', },
role: { type: String, enum: ['admin', 'user'], default: 'user' },
account: { type: Schema.Types.ObjectId, ref: 'account', required: true }
});
const AccountUserRole = mongoose.model('accountUserRole', accountUserRoleSchema);
const permissionSchema = mongoose.Schema({
user : { type: Schema.Types.ObjectId, ref: 'user', required: true },
type: { type: Schema.Types.ObjectId, ref: 'permissionType', required: true },
read: { type: Boolean, default: false, required: true },
write: { type: Boolean, default: false, required: true },
delete: { type: Boolean, default: false, required: true },
accountUser : { type: Schema.Types.ObjectId, ref: 'account',required: true }
});
const Permission = mongoose.model('permission', permissionSchema);
const permissionTypeSchema = mongoose.Schema({
name : { type: String, required: true }
});
const PermissionType = mongoose.model('permissionType', permissionTypeSchema);
const organizationSchema = mongoose.Schema({
account : { type: Schema.Types.ObjectId, ref: 'account', },
name: { type: String, required: true },
logo: { type: String, required: true }
});
const Organization = mongoose.model('organization', organizationSchema);
Now i am developing Authorisation part where the user need to be restricted access to the resource by checking the permission he or she is assigned with .
The solution i found was to develop a Authorisation middleware which run after the authentication middleware which check for the access permissions assigned
But the problem appeared while i tried to access account data based on the user currently logged in , as i will have to search document based on the objectId reference . And i could understand that this could happen if i continue with my current design .This works fine but searching document using objectId reference seems not be a good idea
Authorization middleware
module.exports = {
checkAccess : (permission_type,action) => {
return async (req, res, next) => {
// check if the user object is in the request after verifying jwt
if(req.user){
// find the accountUserRole with the user data from the req after passort jwt auth
const accountUser = await AccountUserRole.findOne({ user :new ObjectId( req.user._id) }).populate('account');
if(accountUser)
{
// find the account and check the type
if(accountUser.account)
{
if(accountUser.account.type === 'single')
{
// if account is single grant access
return next();
}
else if(accountUser.account.type === 'organization'){
// find the user permission
// check permission with permission type and see if action is true
// if true move to next middileware else throw access denied error
}
}
}
}
}
}
}
I decided to scrap my current schema as i understand that forcing RDBMS approach on NoSQL is a bad idea.
Unlike relational databases, with MongoDB the best schema design depends a lot on how you're going to be accessing the data. What will you be using the Account data for, and how will you be accessing it
My new redesigned schema and models
const userSchema = new Schema({
first_name: String,
last_name: String,
email: String,
phone: String,
avatar: String,
password: String,
active: Boolean
account : { type: Schema.Types.ObjectId, ref: 'account', },
role: { type: String, enum: ['admin', 'user'], default: 'user' },
permssion: [
{
type: { type: Schema.Types.ObjectId, ref: 'permissionType', required: true },
read: { type: Boolean, default: false, required: true },
write: { type: Boolean, default: false, required: true },
delete: { type: Boolean, default: false, required: true },
}
]
});
const User = mongoose.model('user', userSchema);
const accountSchema = mongoose.Schema({
account_type: { type: String, enum: ['single', 'organization'], default: 'single' },
organization: {
name: { type: String, required: true },
logo: { type: String, required: true }
},
billing_address: String,
shipping_address: String,
});
const Account = mongoose.model('account', accountSchema);
const permissionTypeSchema = mongoose.Schema({
name : { type: String, required: true }
});
const PermissionType = mongoose.model('permissionType', permissionTypeSchema);
Still i am not sure if this is the right way to do it , please help me with you suggestions.
you can merge user and user account schema :
added some more fileds which is useful to you .
const userSchema = new Schema({
first_name: { type: String,default:'',required:true},
last_name: { type: String,default:'',required:true},
email: { type: String,unique:true,required:true,index: true},
email_verified :{type: Boolean,default:false},
email_verify_token:{type: String,default:null},
phone: { type: String,default:''},
phone_verified :{type: Boolean,default:false},
phone_otp_number:{type:Number,default:null},
phone_otp_expired_at:{ type: Date,default:null},
avatar: { type: String,default:''},
password: { type: String,required:true},
password_reset_token:{type: String,default:null},
reset_token_expired_at: { type: Date,default:null},
active: { type: Boolean,default:true}
account_type: { type: String, enum: ['single', 'organization'], default: 'single' },
organization: {type:Schema.Types.Mixed,default:{}},
billing_address: { type: String,default:''}
shipping_address: { type: String,default:''}
role: { type: String, enum: ['admin', 'user'], default: 'user' },
permission: [
{
type: { type: Schema.Types.ObjectId, ref: 'permissionType', required: true },
read: { type: Boolean, default: false, required: true },
write: { type: Boolean, default: false, required: true },
delete: { type: Boolean, default: false, required: true },
}
],
created_at: { type: Date, default: Date.now },
updated_at: { type: Date, default: Date.now }
});
in your middleware :
module.exports = {
checkAccess : (permission_type,action) => {
return async (req, res, next) => {
// check if the user object is in the request after verifying jwt
if(req.user){
if(req.user.account_type === 'single')
{
// if account is single grant access
return next();
}
else{
// find the user permission
// check permission with permission type and see if action is true
// if true move to next middileware else throw access denied error
}
}
}
}
};
I would suggest:
1 - Define your permission levels, for example: If the user is assigned to a specific Role / Permission level, what features/options he can access.
2 - Permission levels should be recognized by Number (1 = Admin, 2 = User) etc and that key should be indexed in MongoDB (You can use and rely on the ObjectID as well).
3 - Your user object/schema should only have a permission key with the type of Number in Mongoose - no need to create a separate schema for this.
const userSchema = new Schema({
first_name: String,
last_name: String,
email: String,
phone: String,
avatar: String,
password: String,
active: Boolean
account : { type: Schema.Types.ObjectId, ref: 'account', },
permssion: {type: Number, required: true, default: 2} // Default's User
});
With this approach, you can modify your auth check middleware to just check if the permission level sent by the client is identified by the DB and if it does, give the user access else throw access denied error.
If you want you can add another field with permission type and return the name of the permission as well but I think you should handle it on the client, not on the server / be.
I partially understood the requirements (Bad at reading too many words) so I have left anything untouched, let me know.

Mongoose unable to get _id from Schema

I 've a UserSchema that looks like:
export var UserSchema: Schema = new mongoose.Schema({
createdAt: Date,
email: {
type: String,
required: true,
trim: true,
unique: false,
},
firstName: {
type: String,
required: false,
trim: true
},
lastName: {
type: String,
required: false,
trim: true
},
password: {
type: String,
trim: true,
minlength: 6
},
tokens: [{
access: {
type: String,
required: true
},
token: {
type: String,
required: true
}
}]
});
And I 've a instance method like:
UserSchema.methods.printThis = () => {
var user = this;
console.log("========>>> PRINTING USER NOW");
console.log(user);
};
The method printThis is being called from
router.post('/signup', (req, res) => {
var body = _.pick(req.body, ['email', 'password']);
var user = new User(body);
console.log("created user as: ", user);
user.printThis();
});
Below is the output:
created user as: { email: 'prsabodh.r#gmail.com',
password: '123456',
_id: 59be50683606a91647b7a738,
tokens: [] }
========>>> PRINTING USER NOW
{}
You can see that the user is getting created properly. However, when I call printThis method on User - I'm not able to print the same user back and an empty {} is printed. How to fix this?
You shouldn't use arrow functions (=>) if the calling function is explicitly setting a context (which is what Mongoose does):
UserSchema.methods.printThis = function() {
var user = this;
console.log("========>>> PRINTING USER NOW");
console.log(user);
};
More info on arrow functions and their handling of this here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#Arrow_functions
To get the _id value from the instance method can use _conditions that should work
UserSchema.methods.printThis = function(password) {
var user = this;
console.log(user._conditions['_id']);
};

Get info from 2 separate Mongo documents in one mongoose query

Im using MongoDb, and I have a workspace schema with mongoose (v4.0.1):
var Workspace = new mongoose.Schema({
name: {
type: String,
required: true
},
userId: {
type: String,
required: true
},
createdOn: {
type: Date,
"default": Date.now
}
});
And a user schema:
var User = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true
},
organisation: {
type: String,
required: true
},
location: {
type: String,
required: true
},
verifyString: {
type: String
},
verified: {
type: Boolean,
default: false
},
password: {
type: String,
required: true
},
createdOn: {
type: Date,
"default": Date.now
},
isAdmin: {
type: Boolean,
default: false
}
});
So the Workspace userId is the ObjectID from the User document.
When Im logged in as an adminstrator, I want to get all workspaces, as well as the email of the user that owns the workspace.
What Im doing is getting very messy:
Workspace.find({}).exec.then(function(workspaceObects){
var userPromise = workspaceObects.map(function(workspaceObect){
// get the user model with workspaceObect.userId here
});
// somehow combine workspaceObjects and users
});
The above doesnt work and gets extremely messy. Basically I have to loop through the workspaceObjects and go retrieve the user object from the workspace userId. But because its all promises and it becomes very complex and easy to make a mistake.
Is there a much simpler way to do this? In SQL it would require one simple join. Is my schema wrong? Can I get all workspaces and their user owners email in one Mongoose query?
var Workspace = new mongoose.Schema({
userId: {
type: String,
required: true,
ref: 'User' //add this to your schema
}
});
Workspace.find().populate('userId').exec( (err, res) => {
//you will have res with all user fields
});
http://mongoosejs.com/docs/populate.html
Mongo don't have joins but mongoose provides a very powerfull tool to help you with you have to change the model a little bit and use populate:
Mongoose population
You have to make a few changes to your models and get the info of the user model inside your workspace model.
Hope it helps

Resources