Mongodb - Users, roles and groups with JWT + REST API - mongodb

Intro
I am implementing simple MEAN app with JWT token for auth. This app allows you to create account and then create your own group or join existing one. Each group have specific roles that group administrator assign to each user. Here is my Schemas, that will hopefully help you better understand my solution.
User schema
_id: {
type: String,
default: function () { return new mongo.ObjectId().toString() }
},
email: {
type: String,
required: true,
unique: true
},
displayName: {
type: String
},
hash: {
required: true,
type: String
}
Group schema
_id: {
type: String,
default: function () { return new mongo.ObjectId().toString() }
},
groupName: String,
ownerUser: {
type: String,
ref: 'User'
},
userList: [
{
user: {
type: String,
ref: 'User'
},
role: {
type: String,
ref: 'Role'
}
}
]
Role schema
_id: {
type: String,
default: function () { return new mongo.ObjectId().toString() }
},
groupId: String,
roleName: String,
access: [{
path: String,
allowed: Boolean
}]
How it works
User register/login -> Create group or join existing one -> Group admin assign role to user.
Groups will have approximately 250 users.
Group could have multiple roles (10-15).
User can join multiple groups
Express part
example route
router.get('/restricted', JWTAuth, userProject, userRole, (res, req, next) => {
res.send("ok")
})
access.js
export const JWTAuth = passportAuth('jwt', {session: false});
export const userProject = async (req, res, next) => {
const userProject = await GroupModel.findOne({ _id: req.body.groupId, "userList.user" : req.user._id })
if(userProject) {
req.project = userProject;
next()
}else{
res.sendStatus(401);
}
}
export const userRole = async (req, res, next) => {
const roleId = arrayFindByObjectValue(req.user._id, 'user', req.project.userList).role;
const userRole = await RoleModel.findOne({ _id: roleId })
req.role = userRole;
next();
}
export const arrayFindByObjectValue = (value, key, array) => {
for (var i = 0; i < array.length; i++) {
if (array[i][key] === value) {
return array[i];
}
}
}
other tries..
populate (any improvements at speed)
export const userRolePopulate = async (req, res, next) => {
const userProject = await GroupModel.findOne({ _id: req.body.groupId, "userList.user" : req.user._id }, {"userList.$" : 1}).populate('userList.role');
req.role = userProject.userList[0].role;
next()
}
and aggregation (15% improvement)
const userProject: any = await GroupModel.aggregate([
{$match: {_id: req.body.groupId}},
{$project: {
userList: {$filter: {
input: '$userList',
as: 'user',
cond: {$eq: ['$$user.user', req.user._id]}
}},
_id: 0
}},
])
Question
Now I hope you understand my situation. With this approach I have to do multiple db queries to get user role just for simple request. I am worried about the speed. Each request took around 150 ms more with role middleware (1000 Group users). I think this solution will unnecessarily overloads the server. Is there better solution? Should I use some session(stateless JWT..) or other method to temporary store user role? Is there better database design for this method?
I read a lot of articles about role based systems, but I have never came across this specific solution. Thank you for your answers. If you need more informations please let me know and I will edit my answer.

Related

How to update a user profile which has a property which is a ref in MongooseJS?

I have a User schema which has reference to a profile schema.
const UserSchema = new Schema(
{
_id: mongoose.Schema.Types.ObjectId,
email: {
....email props...
},
password: {
...password props...
},
profile: [{
type: mongoose.Schema.Types.ObjectId,
ref: "Profile",
}],
},
);
const Profile = new Schema({
_user: {
type: Schema.Types.ObjectId, ref: 'User'
},
'displayName': {
type: String,
default: ''
},
'interestedActivities': ['Ping-pong'], <---- This bad boy/girl is an array
'memberSince': { type: Date, default: Date.now }
}
)
I'd like to create a route which can update the User properties AND the Profile properties in one shot—with a caveat one of the properties on the Profile model is an array!!!
I tried this....
handler
.use(auth)
.put((req, res, next) => {
emailValidator(req, res, next, 'email');
},
async (req, res, next) => {
await connectDB()
const {
profileDisplayName,
profileEmail,
interestedActivities } = req.body;
const update = {
email: profileEmail,
'profile.$.displayName': profileDisplayName,
'profile.$.interestedActivities': interestedActivities
}
const filter = { _id: req.user.id };
const updatedUser = await User.findOneAndUpdate(filter, update, { new: true })
try {
console.log("updatedUser ", updatedUser);
if (updatedUser) {
return res.status(200).send({
updatedUser,
msg: `You have updated your profile, good job!`
});
}
} catch (error) {
errorHandler(error, res)
}
})
export default handler;
My response is:
Status Code: 500 Internal Server Error
Cast to ObjectId failed for value "[
{
id: 'ae925393-0935-45da-93cb-7db509aedf20',
name: 'interestedActivities',
value: []
}
]" (type Array) at path "profile.$"
Does anyone know how I could also afford for the property which is an array?
Thank you in advance!

mongoose select query with count from other collections

I am working with node(express) with mongoose and I have two collections,
Users
Comments
I added the sample Schema(added few fields only)
const UserSchema = mongoose.Schema({
name: String,
email: String,
});
const CommentsSchema = mongoose.Schema({
comments: String,
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
text: String,
});
So I trying to fetch the users list and no of comments count based on user..
Expecting output like below:
data = [
{
name: 'abcd',
email: 'aa#test.com',
commentsCount: 5
},
{
name: 'xxx',
email: 'xx#test.com',
commentsCount: 3
}
]
I am not sure how to get the results, because we don;t have ref in user table..
userModel.find({}).exec((err, users) => {
if (err) {
res.send(err);
return;
}
users.forEach(function(user){
commentsModel.countDocuments({user_id: users._id}).exec((err, count) => {
if(!err){
user.commentsCount = count;
}
})
});
console.log('users', users)
});
Can you anyone please help to fix, I needs to list out the users and count of comments

Load nested virtual during mongodb query

I'm new to using a key other than ObjectId to link data from other collections. Currently, I have appointments with various other data I'd like to bring in so I can evaluate whether payment is due or not.
My query worked, except it doesn't bring in the plan information for each patient. I understand that it makes a separate query for each populate, so I'd have to do it after I populate the patient information with populate('patientID'):
const appts = await Appt.find(searchParams)
.populate('patientID')
.populate('patientID.plan')
.populate('status')
.populate('type')
.sort({ scheduled: -1 });
The above doesn't work for bringing in the nested JSON of the plan information, but it DOES work for bringing in the patient collection, status, and type. Only patientID.plan populate doesn't work.
My schemas:
const familySchema = new mongoose.Schema({
ID: {
type: Number,
index: true
},
family: String
});
const paymentplanSchema = new mongoose.Schema({
ID: {
type: Number,
index: true
},
plan: String,
planamt: Number
});
const patientSchema = new mongoose.Schema({
ID: {
type: Number
},
familyID: Number,
first: String,
last: String,
careplanID: Number,
otherData: variousTypes
});
patientSchema.virtual('plan', {
ref: 'PaymentPlan', // The model to use
localField: 'careplanID', // Find people where `localField`
foreignField: 'ID' // is equal to `foreignField`
});
patientSchema.pre('find', function() {
this.populate('plan');
});
const typeSchema = new mongoose.Schema({
ID: Number,
appttype: String,
abbr: String,
amt: Number,
code: String,
length: Number
});
const statusSchema = new mongoose.Schema({
ID: Number,
status: String
});
const apptSchema = new mongoose.Schema({
ID: Number,
patientID: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Patient'
},
oldPatientID: Number,
status: {
type: mongoose.Schema.Types.ObjectId,
ref: 'ApptStatus'
},
type: {
type: mongoose.Schema.Types.ObjectId,
ref: 'ApptType'
},
scheduled: Date,
note: String
});
mongoose.model('Appt', apptSchema);
mongoose.model('ApptStatus', statusSchema);
mongoose.model('ApptType', typeSchema);
mongoose.model('Patient', patientSchema);
mongoose.model('PaymentPlan', paymentplanSchema);
How do I get the patient data to load WITH the plan data? I don't get what I'm doing wrong, and I've got other things I'd like to connect in this way (via index instead of ObjectId) but just don't get what I'm doing wrong.
UPDATED TO ADD MORE DETAIL:
My query on the backend to get the appointments is this:
module.exports.search = async (req, res) => {
console.log('GET the appts');
const searchParams =
req.params.query === 'today'
? { scheduled: { $gt: new Date(dayStart), $lt: new Date(dayEnd) } }
: req.body;
console.log(searchParams);
try {
const appts = await Appt.find(searchParams)
.populate({
path: 'patientID',
populate: { path: 'plan' }
})
.populate('status')
.populate('type')
.sort({ scheduled: -1 });
if (!appts) {
console.log(`No appointments found`);
}
appts.forEach(p => {
const patient = p.patientID ? p.patientID.nickname : 'NONE';
const plan =
p.patientID && p.patientID.plan ? p.patientID.plan.planamt : 0;
console.log(patient, plan);
});
console.log(appts.length, 'appts found');
res.send(appts);
} catch (err) {
console.log(`Error`, err);
return res.status(500).send(err);
}
};
In the console, It's logging correctly (example):
CarF 60
8075 'appts found'
In the frontend, all the objects are populated EXCEPT patientID.plan. The patientID object does not include a plan field on any of the entries. patientID, status, and type all populated the corresponding objects.
WHY is this logging on the backend, but not visible on the frontend?
You should be able to do it by passing a path option to populate():
const appts = await Appt.find(searchParams)
.populate('patientID')
.populate({
path: 'patientID',
populate: {path: 'plan'}
})
.populate('status')
.populate('type')
.sort({ scheduled: -1 });
See https://mongoosejs.com/docs/populate.html#deep-populate in official docs

How to get All items associated to a user in mongoose

I have a collection of Items that a particular make and perform a transaction. In my schema, I associate the userId to each item. I want to be able to display as a list all the items that the user owns.
Here I have managed to total up all sizes of each item but I cant work out a way how to get a total for each user
{
id: Number,
x: Number,
y: Number,
xSize: String,
ySize: String,
imageSource: String,
user: { type: mongoose.Schema.ObjectId, ref: 'User' }
},
{ timestamps: true }
);
const UserSchema = new Schema(
{
id: Number,
name: String,
website: String,
},
{ timestamps: true }
);
Item.find({}, function (err, items) {
var itemMap = {};
items.forEach(function (item) {
itemMap[item._id] = item;
});
var countedNames = items.reduce(function (allNames, name) {
if (name.xSize in allNames) {
allNames[name.xSize]++;
}
else {
allNames[name.xSize] = 1;
}
return allNames;
}, {});
Essentially i want to get a list basically saying
{name:"Dave", website:"www.google.com, items:[item1, item2]}
where item1 and item2 relate to the item schema
You should rewrite your UserSchema to contain a reference to the item, in this format:
const UserSchema = new Schema(
{
id: Number,
name: String,
website: String,
items:
[{
item: {type: mongoose.Schema.ObjectId, ref: 'Item'}
}]
},
{ timestamps: true }
);
This will simply allow you to perform the following query:
User.find({}).populate('Item')
Which would return the User document, and all items associated under the document.
You could do the following:
let users = User
.find({})
.populate('Item')
.exec(function (err, users) {
if (err) { console.log(err); }
console.log(users)
}
Rewriting the schema will make querying users for their items much easier.

MongoDB & Mongoose: unable to populate a user's posts with .populate()

I've searched this site for days looking through the many different but similar questions on this topic to no avail.
Here's what I'd like to happen. A user signs in and their posts are automatically linked to the users collection. Eventually I'd like to link posts to the profile it was posted to, but i"m not quite there yet. Here's what I've tried so far.
In the User Schema:
const UserSchema = new Schema({
posts: [{
type: Schema.Types.ObjectId,
ref: 'posts'
}],
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
...
});
module.exports = User = mongoose.model('users', UserSchema);
In the Post Schema:
const PostSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
text: {
type: String,
required: true
},
name: {
type: String
},
...
});
module.exports = Post = mongoose.model('posts', PostSchema);
In my users api, here's how I'm signing the user in and attempting to populate the user's posts:
const User = require('../../models/User');
router.post('/login', (req, res) => {
const { errors, isValid } = validateLoginInput(req.body);
// Check Validation
if (! isValid) {
return res.status(400).json(errors);
}
const email = req.body.email;
const password = req.body.password;
// Find user by email
User.findOne({ email })
.populate('posts')
.then(user => {
if (! user) {
errors.email = 'User not found';
return res.status(400).json(errors);
}
// Check password
bcrypt.compare(password, user.password).then(isMatch => {
if (isMatch) {
// User Matched
// Create JWT Payload
const payload = {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
name: user.firstName + ' ' + user.lastName,
avatar: user.avatar,
posts: user.posts
};
jwt.sign(
payload,
keys.secretOrKey,
{ expiresIn: 3600 }, (err, token) => {
res.json({
success: true,
token: 'Bearer ' + token,
payload
});
});
} else {
errors.password = 'Password is incorrect';
return res.status(400).json(errors);
}
});
});
});
In the posts api, here's how the post is being submitted:
router.post('/', passport.authenticate('jwt', { session: false }), (req, res) => {
const { errors, isValid } = validatePostInput(req.body);
if (! isValid) {
// Return errors with 400 status
return res.status(400).json(errors)
}
const newPost = new Post({
text: req.body.text,
name: req.body.name,
avatar: req.body.avatar,
user: req.user.id
});
newPost.save().then(post => res.json(post));
});
Currently, all I'm seeing is an empty array and no errors. I've been spinning my wheels on this one for a couple days now so any help would be appreciated. Thanks!
I think you forgot to save the _id of your new post to the User model so that the populate() can lookup the posts to populate:
newPost.save().then(post => {
User.update({ _id: req.user.id }, { $push: { posts: post._id }}, (err) => {
res.json(post));
});
});