From 27b74eb9e1ba5e41c43f4eea9c40db59e5cdb76e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Wed, 17 Aug 2022 12:11:52 +0200 Subject: [PATCH] Implement 'GroupMember' resolver, a first step --- .../src/middleware/permissionsMiddleware.js | 32 +++++ backend/src/schema/resolvers/groups.js | 22 ++++ backend/src/schema/resolvers/groups.spec.js | 122 ++++++++++++++++++ backend/src/schema/types/type/Group.gql | 8 ++ backend/src/schema/types/type/User.gql | 2 + 5 files changed, 186 insertions(+) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 99dcfc0cd..4c73624b0 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -52,6 +52,37 @@ const isMySocialMedia = rule({ return socialMedia.ownedBy.node.id === user.id }) +const isAllowSeeingMembersOfGroup = rule({ + cache: 'no_cache', +})(async (_parent, args, { user, driver }) => { + if (!user) return false + const { id: groupId } = args + const session = driver.session() + const readTxPromise = session.readTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (group:Group {id: $groupId}) + OPTIONAL MATCH (admin {id:User $userId})-[membership:MEMBER_OF]->(group) + WHERE membership.role IN ['admin', 'owner'] + RETURN group, admin + `, + { groupId, userId: user.id }, + ) + return { + admin: transactionResponse.records.map((record) => record.get('admin')), + group: transactionResponse.records.map((record) => record.get('group')), + } + }) + try { + const [{ admin, group }] = await readTxPromise + return group.groupType === 'public' || !!admin + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +}) + const isAuthor = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { @@ -115,6 +146,7 @@ export default shield( statistics: allow, currentUser: allow, Group: isAuthenticated, + GroupMember: isAllowSeeingMembersOfGroup, Post: allow, profilePagePosts: allow, Comment: allow, diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 5737f5505..d27d9b5e4 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -46,6 +46,28 @@ export default { session.close() } }, + GroupMember: async (_object, params, context, _resolveInfo) => { + const { id: groupId } = params + const session = context.driver.session() + const readTxResultPromise = session.readTransaction(async (txc) => { + const groupMemberCypher = ` + MATCH (user:User {id: $userId})-[membership:MEMBER_OF]->(:Group {id: $groupId}) + RETURN user {.*, myRoleInGroup: membership.role} + ` + const result = await txc.run(groupMemberCypher, { + groupId, + userId: context.user.id, + }) + return result.records.map((record) => record.get('user')) + }) + try { + return await readTxResultPromise + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, }, Mutation: { CreateGroup: async (_parent, params, context, _resolveInfo) => { diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 707558a06..706e27748 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -207,6 +207,128 @@ describe('Group', () => { }) }) +describe('GroupMember', () => { + describe('unauthenticated', () => { + it('throws authorization error', async () => { + const { errors } = await query({ query: groupQuery, variables: {} }) + expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + let otherUser + + beforeEach(async () => { + otherUser = await Factory.build( + 'user', + { + id: 'other-user', + name: 'Other TestUser', + }, + { + email: 'test2@example.org', + password: '1234', + }, + ) + authenticatedUser = await otherUser.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'others-group', + name: 'Uninteresting Group', + about: 'We will change nothing!', + description: 'We love it like it is!?' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'global', + categoryIds, + }, + }) + authenticatedUser = await user.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'my-group', + name: 'The Best Group', + about: 'We will change the world!', + description: 'Some description' + descriptionAdditional100, + groupType: 'public', + actionRadius: 'regional', + categoryIds, + }, + }) + }) + + describe('query group members', () => { + describe('by owner', () => { + it.only('finds all members', async () => { + const expected = { + data: { + GroupMember: expect.arrayContaining([ + expect.objectContaining({ + id: 'my-group', + slug: 'the-best-group', + myRole: 'owner', + }), + // Wolle: expect.objectContaining({ + // id: 'others-group', + // slug: 'uninteresting-group', + // myRole: null, + // }), + ]), + }, + errors: undefined, + } + await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject(expected) + }) + }) + + describe('isMember = true', () => { + it('finds only groups where user is member', async () => { + const expected = { + data: { + Group: [ + { + id: 'my-group', + slug: 'the-best-group', + myRole: 'owner', + }, + ], + }, + errors: undefined, + } + await expect( + query({ query: groupQuery, variables: { isMember: true } }), + ).resolves.toMatchObject(expected) + }) + }) + + describe('isMember = false', () => { + it('finds only groups where user is not(!) member', async () => { + const expected = { + data: { + Group: expect.arrayContaining([ + expect.objectContaining({ + id: 'others-group', + slug: 'uninteresting-group', + myRole: null, + }), + ]), + }, + errors: undefined, + } + await expect( + query({ query: groupQuery, variables: { isMember: false } }), + ).resolves.toMatchObject(expected) + }) + }) + }) + }) +}) + describe('CreateGroup', () => { beforeEach(() => { variables = { diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index 3165b4a44..fd53d48b3 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -74,6 +74,14 @@ type Query { filter: _GroupFilter ): [Group] + GroupMember( + id: ID + first: Int + offset: Int + orderBy: [_GroupOrdering] + filter: _GroupFilter + ): [User] + AvailableGroupTypes: [GroupType]! AvailableGroupActionRadii: [GroupActionRadius]! diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index a25e51079..4219cd00e 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -114,6 +114,8 @@ type User { badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") emotions: [EMOTED] + + myRoleInGroup: GroupMemberRole }