From 67b499825389ce3d8369cfd97e510bcd5a47ff59 Mon Sep 17 00:00:00 2001 From: Ashish Padhy <100484401+Shurtu-gal@users.noreply.github.com> Date: Sat, 23 Sep 2023 01:18:42 +0530 Subject: [PATCH] feat: update permissions --- server/schema/comment/comment.datasources.js | 23 ++++-- server/schema/comment/comment.model.js | 5 ++ server/schema/comment/comment.mutation.js | 10 ++- server/schema/comment/comment.query.js | 9 +-- server/schema/comment/comment.resolver.js | 74 +++++++++++++++----- server/schema/comment/comment.type.js | 6 +- server/utils/userAuth/role.js | 3 +- 7 files changed, 101 insertions(+), 29 deletions(-) diff --git a/server/schema/comment/comment.datasources.js b/server/schema/comment/comment.datasources.js index 740fae1e..1d3bc9d0 100644 --- a/server/schema/comment/comment.datasources.js +++ b/server/schema/comment/comment.datasources.js @@ -6,11 +6,14 @@ const CommentModel = require('./comment.model'); const findByID = () => new DataLoader( - async (ids) => { + async (data) => { try { - const _comments = await CommentModel.find({ _id: ids }); + const _comments = await CommentModel.find({ _id: { $in: data.map(({ id }) => id) } }); - const _returnComments = ids.map((id) => _comments.find((_comment) => _comment.id.toString() === id.toString())); + const _returnComments = data.map(({ id, permission, mid }) => { + const _comment = _comments.find((comment) => comment._id.toString() === id.toString()); + return _comment && _comment.approved ? _comment : permission || mid === _comment.createdBy ? _comment : null; + }); return _returnComments; } catch (error) { throw APIError(null, error); @@ -21,15 +24,21 @@ const findByID = () => } ); -const findAll = (offset, limit) => CommentModel.find().sort({ createdAt: 'desc' }).skip(offset).limit(limit); +const findAll = (offset, limit, permission, mid) => { + // Get approved comments if the user does not have permission to read unapproved comments and the user is not the author + // Get all comments if the user has permission to read unapproved comments or the user is the author + const query = permission ? {} : { $or: [{ approved: true }, { approved: false, createdBy: mid }] }; + return CommentModel.find(query).sort({ createdAt: 'desc' }).skip(offset).limit(limit); +}; const countNumberOfComments = (parentID, parentModel) => CommentModel.countDocuments({ 'parent.reference': parentID, 'parent.model': parentModel, + approved: true, }); -const create = async (authorID, content, parentID, parentType, session, authToken, mid) => { +const create = async (authorID, content, parentID, parentType, session, authToken, mid, approved) => { try { const _author = await userModel.findById(authorID); if (!_author) { @@ -47,6 +56,7 @@ const create = async (authorID, content, parentID, parentType, session, authToke reference: parentID, model: parentType, }, + approved: approved || false, createdBy: UserSession.valid(session, authToken) ? mid : null, }, ]); @@ -74,6 +84,8 @@ const updateContent = async (id, content, session, authToken, mid) => { } }; +const approve = (id) => CommentModel.findByIdAndUpdate(id, { approved: true }, { new: true }); + const remove = (id) => CommentModel.findByIdAndDelete(id); const CommentDataSources = () => ({ @@ -81,6 +93,7 @@ const CommentDataSources = () => ({ findByID: findByID(), countNumberOfComments, create, + approve, updateContent, remove, }); diff --git a/server/schema/comment/comment.model.js b/server/schema/comment/comment.model.js index dc3d40a8..0deb7a36 100644 --- a/server/schema/comment/comment.model.js +++ b/server/schema/comment/comment.model.js @@ -35,6 +35,11 @@ const CommentSchema = new Schema( trim: true, }, }, + approved: { + type: Boolean, + required: false, + default: false, + }, parent: { model: { type: String, diff --git a/server/schema/comment/comment.mutation.js b/server/schema/comment/comment.mutation.js index 0b3eb44c..f0cf6ca2 100644 --- a/server/schema/comment/comment.mutation.js +++ b/server/schema/comment/comment.mutation.js @@ -21,7 +21,7 @@ const { // GraphQLJSONObject, } = require('../scalars'); const { CommentParentModelEmum } = require('./comment.enum.types'); -const { createComment, deleteComment, updateCommentContent } = require('./comment.resolver'); +const { createComment, deleteComment, updateCommentContent, approveComment } = require('./comment.resolver'); const CommentType = require('./comment.type'); @@ -39,6 +39,14 @@ module.exports = new GraphQLObjectType({ }, resolve: createComment, }, + approveComment: { + description: 'Approve a comment', + type: CommentType, + args: { + id: { type: new GraphQLNonNull(GraphQLID) }, + }, + resolve: approveComment, + }, updateCommentContent: { description: 'Update Comment by Id', type: CommentType, diff --git a/server/schema/comment/comment.query.js b/server/schema/comment/comment.query.js index 3af7b189..cadb713c 100644 --- a/server/schema/comment/comment.query.js +++ b/server/schema/comment/comment.query.js @@ -9,7 +9,7 @@ const { GraphQLNonNull, // GraphQLError, GraphQLList, - GraphQLString, + // GraphQLString, GraphQLID, // GraphQLBoolean, GraphQLInt, @@ -20,6 +20,7 @@ const { // GraphQLJSON, // GraphQLJSONObject, } = require('../scalars'); +const { CommentParentModelEmum } = require('./comment.enum.types'); const { getListOfComments, getCommentById, countOfComments } = require('./comment.resolver'); const CommentType = require('./comment.type'); @@ -36,11 +37,11 @@ module.exports = new GraphQLObjectType({ type: new GraphQLList(new GraphQLNonNull(GraphQLID)), }, limit: { - description: 'No of Comments to be retrieved', + description: 'No. of Comments to be retrieved', type: GraphQLInt, }, offset: { - description: 'No of Comments to be skipped | pagination', + description: 'No. of Comments to be skipped | pagination', type: GraphQLInt, }, }, @@ -67,7 +68,7 @@ module.exports = new GraphQLObjectType({ }, parentType: { description: 'Type of parent', - type: new GraphQLNonNull(GraphQLString), + type: new GraphQLNonNull(CommentParentModelEmum), }, }, resolve: countOfComments, diff --git a/server/schema/comment/comment.resolver.js b/server/schema/comment/comment.resolver.js index a5a529a6..f871a8fe 100644 --- a/server/schema/comment/comment.resolver.js +++ b/server/schema/comment/comment.resolver.js @@ -4,7 +4,7 @@ const UserPermission = require('../../utils/userAuth/permission'); const DEF_LIMIT = 10, DEF_OFFSET = 0; -const canMutateComment = async (session, authToken, decodedToken, id, mid, Comment) => { +const canMutateComment = async (session, authToken, decodedToken, id, mid, Comment, needsAdmin = false) => { const _comment = await Comment.findByID.load(id); if (!_comment) { @@ -12,29 +12,58 @@ const canMutateComment = async (session, authToken, decodedToken, id, mid, Comme } if ( - (_comment.author.reference !== mid || - !UserPermission.exists(session, authToken, decodedToken, 'comment.write.new')) && - !UserPermission.exists(session, authToken, decodedToken, 'comment.write.all') + // If the user is not the author of the comment or does not have permission to delete his/her own comment + _comment.createdBy !== mid || + (!UserPermission.exists(session, authToken, decodedToken, 'comment.write.self') && + // Furthermore, if the user is not an admin or does not have permission to delete other's comment + needsAdmin && + !UserPermission.exists(session, authToken, decodedToken, 'comment.write.delete')) ) { throw APIError('FORBIDDEN', null, 'User does not have required permission to update the comment'); } }; +const canReadUnApprovedComments = (session, authToken, decodedToken) => { + try { + if (!UserPermission.exists(session, authToken, decodedToken, 'comment.read.unapproved')) { + return false; + } + + return true; + } catch (error) { + return false; + } +}; + module.exports = { - getListOfComments: async (_parent, { ids = null, limit = DEF_LIMIT, offset = DEF_OFFSET }, { API: { Comment } }) => { + getListOfComments: async ( + _parent, + { ids = null, limit = DEF_LIMIT, offset = DEF_OFFSET }, + { session, authToken, decodedToken, mid, API: { Comment } } + ) => { try { + const permission = canReadUnApprovedComments(session, authToken, decodedToken); + const _comments = ids - ? await Promise.all(ids.slice(offset, offset + limit).map((id) => Comment.findByID.load(id))) - : await Comment.findAll(offset, limit); + ? // Gets approved and unapproved comments of the user if the user does not have permission to read unapproved comments + // Gets all comments if the user has permission to read unapproved comments + // Self comments are always returned regardless of permission to be handled by the client + await Promise.all( + ids.slice(offset, offset + limit).map((id) => Comment.findByID.load({ id, permission, mid })) + ) + : await Comment.findAll(offset, limit, permission, mid); return _comments.filter((comment) => comment); } catch (error) { throw APIError(null, error); } }, - getCommentById: async (_parent, { id }, { API: { Comment } }) => { + getCommentById: async (_parent, { id }, { session, authToken, decodedToken, mid, API: { Comment } }) => { try { - const _comment = await Comment.findByID.load(id); + // Gets only approved comments if the user does not have permission to read unapproved comments + const permission = canReadUnApprovedComments(session, authToken, decodedToken); + + const _comment = await Comment.findByID.load({ id, permission, mid }); if (!_comment) { throw APIError('NOT FOUND', null, { reason: 'Invalid id for comment' }); } @@ -58,14 +87,14 @@ module.exports = { { session, authToken, decodedToken, mid, API: { Comment } } ) => { try { - if ( - !UserPermission.exists(session, authToken, decodedToken, 'comment.write.new') && - !UserPermission.exists(session, authToken, decodedToken, 'comment.write.all') - ) { + if (!UserPermission.exists(session, authToken, decodedToken, 'comment.write.new')) { throw APIError('FORBIDDEN', null, 'User does not have required permission to create comment'); } - const _comment = await Comment.create(authorID, content, parentID, parentType, session, authToken, mid); + // User can write pre-approved comments if they have permission to write approved comments + const approved = UserPermission.exists(session, authToken, decodedToken, 'comment.write.approve'); + + const _comment = await Comment.create(authorID, content, parentID, parentType, session, authToken, mid, approved); return _comment; } catch (error) { @@ -78,7 +107,7 @@ module.exports = { { session, authToken, decodedToken, mid, API: { Comment } } ) => { try { - await canMutateComment(session, authToken, decodedToken, id, mid, Comment); + await canMutateComment(session, authToken, decodedToken, id, mid, Comment, false); const _comment = await Comment.updateContent(id, content, session, authToken, mid); return _comment; @@ -86,9 +115,22 @@ module.exports = { throw APIError(null, error); } }, + approveComment: async (_parent, { id }, { session, authToken, decodedToken, API: { Comment } }) => { + try { + if (!UserPermission.exists(session, authToken, decodedToken, 'comment.approve.all')) { + throw APIError('FORBIDDEN', null, 'User does not have required permission to approve comment'); + } + + const _comment = await Comment.approve(id); + + return _comment; + } catch (error) { + throw APIError(null, error); + } + }, deleteComment: async (_parent, { id }, { session, authToken, decodedToken, mid, API: { Comment } }) => { try { - await canMutateComment(session, authToken, decodedToken, id, mid, Comment); + await canMutateComment(session, authToken, decodedToken, id, mid, Comment, true); const _comment = await Comment.remove(id); diff --git a/server/schema/comment/comment.type.js b/server/schema/comment/comment.type.js index f5d7c419..a82260c2 100644 --- a/server/schema/comment/comment.type.js +++ b/server/schema/comment/comment.type.js @@ -11,7 +11,7 @@ const { // GraphQLList, GraphQLString, GraphQLID, - // GraphQLBoolean, + GraphQLBoolean, GraphQLInt, // GraphQLFloat, // GraphQLDate, @@ -71,6 +71,8 @@ const CommentType = new GraphQLObjectType({ type: AuthorType, }, + approved: { type: GraphQLBoolean }, + parent: { type: ParentType }, createdAt: { type: GraphQLDateTime }, @@ -85,7 +87,7 @@ const ParentUniontype = new GraphQLUnionType({ name: 'ParentUnion', description: 'Union of article and comment for parent of comment', types: [ArticleType, CommentType], - resolveType: (value) => (value.categories ? ArticleType : CommentType), + resolveType: (value) => (value.categories ? 'Article' : 'Comment'), }); module.exports = CommentType; diff --git a/server/utils/userAuth/role.js b/server/utils/userAuth/role.js index 29a49f42..00d35b5c 100644 --- a/server/utils/userAuth/role.js +++ b/server/utils/userAuth/role.js @@ -16,7 +16,8 @@ const UserRole = { cache: async (_RoleModel = RoleModel) => { try { const _roles = await _RoleModel.find({}, 'name permissions section', { lean: true }); - fs.writeFileSync('./roles.json', JSON.stringify(_roles)); + // Write the roles to a file for caching with proper formatting + fs.writeFileSync('./roles.json', JSON.stringify(_roles, null, 2)); rolesCacheFile = fs.realpathSync('./roles.json'); return rolesCacheFile; } catch (error) {