Newbie here. I'm trying to create a movie recommendation app and I've been struggling with this authorization problem for a while now. My goal is to have a single user schema with a Boolean 'isAdmin' property to differentiate between ordinary users and admin users. The problem is that when I attempt to query the isAdmin variable in the mutation to ensure that the logged in user passed in from the context has the necessary privileges to perform the operation, I get undefined.
User Schema
const userSchema = new mongoose.Schema({
username: {
type: String,
index: {
unique: true
}
},
email: {
type: String,
required: true,
index: {
unique: true
}
},
password: {
type: String,
required: true
},
contributions: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Movie'
},
isAdmin: {
type: Boolean,
required: true,
default: false
}
})
newMovie mutation
newMovie: async (parent, args, { models, user }) => {
if (!user){
throw new AuthenticationError('You must be signed in to submit a new movie')
}
console.log(user.isAdmin)
if (user && user.isAdmin === false) {
throw new ForbiddenError('You are not qualified')
}
return await models.Movie.create({
title: args.title,
year: args.year,
addedBy: mongoose.Types.ObjectId(user.id)
})
}
I attempted to console log user.isAdmin to see the value but instead I'm getting undefined. I have also tried using enum values 'user and admin' for a property 'roles' with the same result.
After struggling for hours, I realized that loading the user from the context returns an object with only the ObjectId and iat which I believe is the object's date of creation. To access the rest of the properties of the context user, I had to search for the user in models and assign that to a variable from which I can access the rest of the properties.
newMovie: async (parent, args, { models, user }) => {
if(!user){
throw new AuthenticationError('You must be logged in')
}
active = await models.User.findById(user.id)
if(active && active.role !== "ADMIN"){
throw new ForbiddenError('Only users can leave reviews')
}
return await models.Movie.create({
title: args.title,
year: args.year,
addedBy: mongoose.Types.ObjectId(active.id)
})
This was the working solution for this problem.
Related
I have events system with different role for each event (same user could be different role in different events).
I created collection of the users and this is the schema that i used:
const userSchema = new mongoose.Schema(
{
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
permissions: [{
eventId: { type: mongoose.Schema.Types.ObjectId, required: false, ref: 'Event' },
role: { type: String, required: false }
}]
},
{timestamps: true}
);
For check if the user is allowed to get this event I created middleware that need to check if the eventId is exist in the User collection under "permissions"
so this is the code that I was create:
const authorization = async (req, res, next) => {
try {
const eventId = req.params.id;
const token = req.headers.authorization.split(' ')[1]
const tokenDecoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = tokenDecoded.id
console.log(userId)
const userPermissionCheck = await User.find({ _id: userId, 'permissions.eventId': { $in: eventId } } );
console.log(userPermissionCheck)
next();
} catch (error) {
res.status(401).json({ message: 'Auth failed.' })
}
}
My problem is that my find function in the authorization middleware is not working...
What the correct way to search key of object in array with mongoose?
thanks
It seems that you are on the right track from your code, but you do not need the $in operator. You should be able to do the following:
const userPermissionCheck = await User.find({ _id: userId, 'permissions.eventId': eventId });
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);
}
});
I have this schema that have an expiry as a counter measure from felling data base with invalidated users so if the user doesn't do a certain validation his account will be deleted !! if he validates then the expiration gets removed, every thing is ok until now but if the user changes his email (after being activated ) what happens in that user.save() for some reason creates the expiration object again on the already existing model in DB even if the user has already been validated ! why does changing email causing this creation to happened ? I did another actions only the email change is creating this expires I tried to prevent it from happening by setting the expiration to null when changing email but it looks too awkward.
again thanks for the effort.
const Userschema = new mongoose.Schema(
{
User: "",
Name: "",
Email: {
type: 'string',
require: [true, 'Please provide an Email'],
unique: true,
lowercase: true,
validate: [validator.isEmail, 'please provide a valid email ']
}// date the the user has been created !!!
createdAt: {
type: Date,
default: Date.now()
},// exparation is by defult
expiration: {
//unique: true,
type: Date,
default: Date.now(),
expires: 60,
},
}
);
// removing experation logic if the user does a valid validation !
const update2 = await User.findOneAndUpdate(
{ Email: req.body.Email }, {
$unset: {
'expiration': 'expires'
}
})
// Solution but not clean.
const user = await User.findById(req.body.id)
console.log(user, "useeeeeeeeeeeeeeeer");
user.Email = newEmail
user.expiration = ''
user.save()
const user = await User.findOneAndUpdate(
{
'_id': req.body.id,
},
{
'$set': {
Email: newEmail
}
}, { _id: true, new: true }
)
This solved the issue still what is the diffrent between User.findById and using save() , and using find one and update.
I have this model:
const userSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
firstname: String,
lastname: String,
password: {
type: String,
required: true
},
activities: [{
description: {
type: String,
required: true
},
status: String,
from: Date,
to: Date
}]
}, { timestamps: true })
After I get the user with User.findById(), how can I get the ObjectId of an activity inside the activities array so I can perform (for example) delete and update operations?
EDIT 1
I can see every ObjectId with console.log() or in my GUI because MongoDB creates an ObjectId for every element inside the array even if I don't specify the field _id for each element. Here's a sample:
USER ACTIVITIES [
{
_id: 603765c7bb1d1c24cc7f80ff,
description: 'Complete the project',
status: 'pending',
from: 2021-02-25T08:54:31.719Z,
to: 2021-02-25T08:54:31.719Z
},
{
_id: 60377672bb1d1c24cc7f8100,
description: 'Figure out how to get the ObjectId',
status: 'pending',
from: 2021-02-25T10:05:38.144Z,
to: 2021-02-25T10:05:38.144Z
}
]
I'd like to access this _id so I can perform operations on a single activity
I've just found a solution. In my routes file I've added one more parameter in the endpoint's path (the element ID) so I can access it via req.params. Then everything is simple. Here's a sample:
Route
router.delete('/activities/:id/:activityId', userController.deleteActivity);
The logic
exports.deleteActivity = async (req, res, next) => {
try {
const id = req.params.id;
const user = await User.findById(id);
if (user) {
const activityId = req.params.activityId;
await user.activities.pull(activityId);
await user.save();
console.log("ACTIVITY REMOVED! YOUR ACTIVITIES", user.activities);
return res.status(200).json({ activities: user.activities });
}
return res.status(400).json({ message: "USER NOT FOUND!" });
} catch (error) {
console.log(error);
res.status(500).json({ error });
}
};
I am trying to determine how to do asynchronous validation for a Mongoose schema - specifically in this case the username. TMK, to ensure that the username is unique, we have to manually query the database to see if the same username already exists. This is an asynchronous query. However the methodology of having a 'validate:' property for each schema item, seems to ask for a synchronous validation function. In other words, this line:
validate: [validation.usernameValidator, 'not a valid username']
seems to require that usernameValidator be synchronous, and the problem is I need it to be async, for the reason aforementioned.
So, I have a Mongoose schema for a User like so:
var validation = {
usernameValidator: function (candidate) {
return true;
},
passwordValidator: function (candidate) {
return true;
}
};
userSchema = mongoose.Schema({
username: {
type: String,
isUnique: true,
required: true,
validate: [validation.usernameValidator, 'not a valid username']
},
passwordHash: {
type: String,
required: true,
validate: [validation.passwordValidator, 'not a valid password']
},
email: {
type: String,
isUnique: true,
required: true,
validate: [validation.emailValidator, 'not a valid email address']
}
});
userSchema.pre('save', function (next) {
var self = this;
if (!self.isModified('passwordHash')) {
return next();
}
bcrypt.hash(self.passwordPreHash, SALT_WORK_FACTOR, function (err, hash) {
if (err) {
return next(err);
}
else if(hash == null){
return next(new Error('null/undefined hash'));
}
else {
self.passwordHash = hash;
next();
}
});
});
//is the following function my best bet?
userSchema.path('username').validate(function (value, respond){
this.findOne({ username: value }, function (err, user){
if(user) respond(false);
});
}, 'This username has been already registered');
is my only option to leave out the validation.usernameValidator methodology, and validate username with userSchema.path('username').validate..?
Mongoose should handle this provided that you specify unique: true on that field.
For example
userSchema = mongoose.Schema({
username: {
type: String,
unique: true,
required: true
},
passwordHash: {
type: String,
required: true
},
email: {
type: String,
unique: true,
required: true
}
});
ADDITION:
Mongoose will declare a unique index provided that you specify such in your schema(as done in example above). This prevents having to query into mongodb to see if another document has a field of the same value. You can read about it here.
You can read more about Unique Indexes for mongodb here, if you'd like to learn more about their behaviour.
Note: A validation error will not be throw if a non-unique value is provided. See the mongoose docs for more info on this.