From b92241942ef96cd3d9ebf7ccdb664b85391ea673 Mon Sep 17 00:00:00 2001 From: Jeeiii Date: Thu, 28 Mar 2024 18:54:57 +0100 Subject: [PATCH] refactor: separate manual vs apikey logic in group service --- .../src/app/admins/entities/admin.entity.ts | 2 + .../api/src/app/groups/groups.service.test.ts | 644 +++++++++++++++--- apps/api/src/app/groups/groups.service.ts | 157 ++++- apps/api/src/app/groups/groups.utils.test.ts | 107 ++- apps/api/src/app/groups/groups.utils.ts | 17 +- 5 files changed, 814 insertions(+), 113 deletions(-) diff --git a/apps/api/src/app/admins/entities/admin.entity.ts b/apps/api/src/app/admins/entities/admin.entity.ts index 87bea3c3..732c134b 100644 --- a/apps/api/src/app/admins/entities/admin.entity.ts +++ b/apps/api/src/app/admins/entities/admin.entity.ts @@ -2,11 +2,13 @@ import { Column, CreateDateColumn, Entity, + Index, PrimaryColumn, UpdateDateColumn } from "typeorm" @Entity("admins") +@Index(["apiKey"], { unique: true }) export class Admin { @PrimaryColumn({ unique: true }) id: string diff --git a/apps/api/src/app/groups/groups.service.test.ts b/apps/api/src/app/groups/groups.service.test.ts index ea8db959..3e2f11fb 100644 --- a/apps/api/src/app/groups/groups.service.test.ts +++ b/apps/api/src/app/groups/groups.service.test.ts @@ -416,7 +416,6 @@ describe("GroupsService", () => { it("Should create a group via API", async () => { const group = await groupsService.createGroupWithAPIKey( groupDto, - admin.id, apiKey ) @@ -432,15 +431,10 @@ describe("GroupsService", () => { it("Should remove a group via API", async () => { const group = await groupsService.createGroupWithAPIKey( groupDto, - admin.id, apiKey ) - await groupsService.removeGroupWithAPIKey( - group.id, - admin.id, - apiKey - ) + await groupsService.removeGroupWithAPIKey(group.id, apiKey) const fun = groupsService.getGroup(group.id) @@ -450,69 +444,51 @@ describe("GroupsService", () => { }) it("Should not create a group if the admin does not exist", async () => { - const fun = groupsService.createGroupWithAPIKey( - groupDto, - "wrong", - apiKey - ) + const fun = groupsService.createGroupWithAPIKey(groupDto, "wrong") - await expect(fun).rejects.toThrow(`Invalid admin for the groups`) + await expect(fun).rejects.toThrow( + `Invalid API key or invalid admin for the groups` + ) }) it("Should not remove a group if the admin does not exist", async () => { const group = await groupsService.createGroupWithAPIKey( groupDto, - admin.id, apiKey ) - const fun = groupsService.removeGroupWithAPIKey( - group.id, - "wrong", - apiKey - ) + const fun = groupsService.removeGroupWithAPIKey(group.id, "wrong") - await expect(fun).rejects.toThrow(`Invalid admin for the groups`) + await expect(fun).rejects.toThrow( + `Invalid API key or invalid admin for the groups` + ) }) it("Should not create a group if the API key is invalid", async () => { - const fun = groupsService.createGroupWithAPIKey( - groupDto, - admin.id, - "apiKey" - ) + const fun = groupsService.createGroupWithAPIKey(groupDto, "apiKey") await expect(fun).rejects.toThrow( - `Invalid API key or API access not enabled for admin '${admin.id}'` + `Invalid API key or invalid admin for the groups` ) }) it("Should not remove a group if the API key is invalid", async () => { const group = await groupsService.createGroupWithAPIKey( groupDto, - admin.id, apiKey ) - const fun = groupsService.removeGroupWithAPIKey( - group.id, - admin.id, - "wrong" - ) + const fun = groupsService.removeGroupWithAPIKey(group.id, "wrong") await expect(fun).rejects.toThrow( - `Invalid API key or API access not enabled for admin '${admin.id}'` + `Invalid API key or invalid admin for the groups` ) }) it("Should not create a group if the API key is disabled for the admin", async () => { await adminsService.updateApiKey(admin.id, ApiKeyActions.Disable) - const fun = groupsService.createGroupWithAPIKey( - groupDto, - admin.id, - apiKey - ) + const fun = groupsService.createGroupWithAPIKey(groupDto, apiKey) await expect(fun).rejects.toThrow( `Invalid API key or API access not enabled for admin '${admin.id}'` @@ -524,17 +500,12 @@ describe("GroupsService", () => { const group = await groupsService.createGroupWithAPIKey( groupDto, - admin.id, apiKey ) await adminsService.updateApiKey(admin.id, ApiKeyActions.Disable) - const fun = groupsService.removeGroupWithAPIKey( - group.id, - admin.id, - apiKey - ) + const fun = groupsService.removeGroupWithAPIKey(group.id, apiKey) await expect(fun).rejects.toThrow( `Invalid API key or API access not enabled for admin '${admin.id}'` @@ -546,7 +517,6 @@ describe("GroupsService", () => { const group = await groupsService.createGroupWithAPIKey( groupDto, - admin.id, apiKey ) @@ -564,7 +534,6 @@ describe("GroupsService", () => { const fun = groupsService.removeGroupWithAPIKey( group.id, - anotherAdmin.id, anotherApiKey ) @@ -619,7 +588,6 @@ describe("GroupsService", () => { it("Should create the groups via API", async () => { const groups = await groupsService.createGroupsWithAPIKey( groupsDtos, - admin.id, apiKey ) @@ -644,11 +612,7 @@ describe("GroupsService", () => { expect(groups).toHaveLength(4) - await groupsService.removeGroupsWithAPIKey( - [ids[0], ids[1]], - admin.id, - apiKey - ) + await groupsService.removeGroupsWithAPIKey([ids[0], ids[1]], apiKey) groups = await groupsService.getGroups({ adminId: admin.id @@ -677,55 +641,45 @@ describe("GroupsService", () => { it("Should not create the groups if the admin does not exist", async () => { const fun = groupsService.createGroupsWithAPIKey( groupsDtos, - "wrong", - apiKey + "wrong" ) - await expect(fun).rejects.toThrow(`Invalid admin for the groups`) + await expect(fun).rejects.toThrow( + `Invalid API key or invalid admin for the groups` + ) }) it("Should not remove the groups if the admin does not exist", async () => { - const fun = groupsService.removeGroupsWithAPIKey( - ids, - "wrong", - apiKey - ) + const fun = groupsService.removeGroupsWithAPIKey(ids, "wrong") - await expect(fun).rejects.toThrow(`Invalid admin for the groups`) + await expect(fun).rejects.toThrow( + `Invalid API key or invalid admin for the groups` + ) }) it("Should not create the groups if the API key is invalid", async () => { const fun = groupsService.createGroupsWithAPIKey( groupsDtos, - admin.id, - "apiKey" + "wrong" ) await expect(fun).rejects.toThrow( - `Invalid API key or API access not enabled for admin '${admin.id}'` + `Invalid API key or invalid admin for the groups` ) }) it("Should not remove the groups if the API key is invalid", async () => { - const fun = groupsService.removeGroupsWithAPIKey( - ids, - admin.id, - "apiKey" - ) + const fun = groupsService.removeGroupsWithAPIKey(ids, "wrong") await expect(fun).rejects.toThrow( - `Invalid API key or API access not enabled for admin '${admin.id}'` + `Invalid API key or invalid admin for the groups` ) }) it("Should not create the groups if the API key is disabled for the admin", async () => { await adminsService.updateApiKey(admin.id, ApiKeyActions.Disable) - const fun = groupsService.createGroupsWithAPIKey( - groupsDtos, - admin.id, - apiKey - ) + const fun = groupsService.createGroupsWithAPIKey(groupsDtos, apiKey) await expect(fun).rejects.toThrow( `Invalid API key or API access not enabled for admin '${admin.id}'` @@ -733,11 +687,7 @@ describe("GroupsService", () => { }) it("Should not remove the groups if the API key is disabled for the admin", async () => { - const fun = groupsService.removeGroupsWithAPIKey( - ids, - admin.id, - apiKey - ) + const fun = groupsService.removeGroupsWithAPIKey(ids, apiKey) await expect(fun).rejects.toThrow( `Invalid API key or API access not enabled for admin '${admin.id}'` @@ -759,7 +709,6 @@ describe("GroupsService", () => { const fun = groupsService.removeGroupsWithAPIKey( [ids[2]], - anotherAdmin.id, anotherApiKey ) @@ -805,7 +754,6 @@ describe("GroupsService", () => { const updatedGroup = await groupsService.updateGroupWithApiKey( group.id, updateDto, - admin.id, apiKey ) @@ -825,25 +773,23 @@ describe("GroupsService", () => { const fun = groupsService.updateGroupWithApiKey( groupId, groupDto, - "wrong-admin", apiKey ) await expect(fun).rejects.toThrow( - `Invalid admin for the group '${groupId}'` + `You are not the admin of the group '${groupId}'` ) }) it("Should not update a group if the group does not exist", async () => { const fun = groupsService.updateGroupWithApiKey( - "wrong-group", + "wrong", groupDto, - admin.id, apiKey ) await expect(fun).rejects.toThrow( - `Group with id 'wrong-group' does not exist` + `Group with id 'wrong' does not exist` ) }) @@ -851,12 +797,11 @@ describe("GroupsService", () => { const fun = groupsService.updateGroupWithApiKey( groupId, groupDto, - admin.id, "invalid-apikey" ) await expect(fun).rejects.toThrow( - `Invalid API key or API access not enabled for admin '${admin.id}'` + `Invalid API key or invalid admin for the groups` ) }) @@ -866,7 +811,132 @@ describe("GroupsService", () => { const fun = groupsService.updateGroupWithApiKey( groupId, groupDto, + apiKey + ) + + await expect(fun).rejects.toThrow( + `Invalid API key or API access not enabled for admin '${admin.id}'` + ) + }) + }) + + describe("# Update groups via API", () => { + const groupsDtos: Array = [ + { + id: "1", + name: "Group1", + description: "This is a new group1", + treeDepth: 16, + fingerprintDuration: 3600 + }, + { + id: "2", + name: "Group2", + description: "This is a new group2", + treeDepth: 32, + fingerprintDuration: 7200 + } + ] + + const updateDtos: Array = [ + { + description: "This is a new new group1", + treeDepth: 32, + fingerprintDuration: 7200 + }, + { + description: "This is a new new group2", + treeDepth: 32, + fingerprintDuration: 14400 + } + ] + let admin: Admin + let apiKey: string + let groups: Array + let groupId1: string + let groupId2: string + + beforeAll(async () => { + admin = await adminsService.create({ + id: "admin", + address: "0x" + }) + + admin = await adminsService.findOne({ id: admin.id }) + apiKey = await adminsService.updateApiKey( admin.id, + ApiKeyActions.Generate + ) + groups = await groupsService.createGroupsManually( + groupsDtos, + admin.id + ) + groupId1 = groups[0].id + groupId2 = groups[1].id + }) + + it("Should update the groups via API", async () => { + const updatedGroups = await groupsService.updateGroupsWithApiKey( + [groupId1, groupId2], + updateDtos, + apiKey + ) + + updatedGroups.forEach((updatedGroup: Group, i: number) => { + expect(updatedGroup.id).toBe(groupsDtos[i].id) + expect(updatedGroup.adminId).toBe(admin.id) + expect(updatedGroup.description).toBe(updateDtos[i].description) + expect(updatedGroup.name).toBe(groupsDtos[i].name) + expect(updatedGroup.treeDepth).toBe(updateDtos[i].treeDepth) + expect(updatedGroup.fingerprintDuration).toBe( + updateDtos[i].fingerprintDuration + ) + expect(updatedGroup.members).toHaveLength(0) + }) + }) + + it("Should not update the groups if the admin is the wrong one", async () => { + const fun = groupsService.updateGroupsWithApiKey( + [groupId1, groupId2], + groupsDtos, + "wrong" + ) + + await expect(fun).rejects.toThrow( + `Invalid API key or invalid admin for the groups` + ) + }) + + it("Should not update the groups if the group does not exist", async () => { + const fun = groupsService.updateGroupsWithApiKey( + ["wrong"], + groupsDtos, + apiKey + ) + + await expect(fun).rejects.toThrow( + `Group with id 'wrong' does not exist` + ) + }) + + it("Should not update the groups if the API key is invalid", async () => { + const fun = groupsService.updateGroupsWithApiKey( + [groupId1, groupId2], + groupsDtos, + "invalid-apikey" + ) + + await expect(fun).rejects.toThrow( + `Invalid API key or invalid admin for the groups` + ) + }) + + it("Should not update the groups if the API key is disabled", async () => { + await adminsService.updateApiKey(admin.id, ApiKeyActions.Disable) + + const fun = groupsService.updateGroupsWithApiKey( + [groupId1, groupId2], + groupsDtos, apiKey ) @@ -1283,6 +1353,402 @@ describe("GroupsService", () => { }) }) + describe("# createGroupManually", () => { + const groupDto: CreateGroupDto = { + id: "1", + name: "Group1", + description: "This is a new group1", + treeDepth: 16, + fingerprintDuration: 3600 + } + let admin: Admin + + beforeAll(async () => { + admin = await adminsService.create({ + id: "admin", + address: "0x" + }) + + admin = await adminsService.findOne({ id: admin.id }) + }) + + it("Should create a group manually", async () => { + const group = await groupsService.createGroupManually( + groupDto, + admin.id + ) + + expect(group.id).toBe(groupDto.id) + expect(group.adminId).toBe(admin.id) + expect(group.description).toBe(groupDto.description) + expect(group.name).toBe(groupDto.name) + expect(group.treeDepth).toBe(groupDto.treeDepth) + expect(group.fingerprintDuration).toBe(groupDto.fingerprintDuration) + expect(group.members).toHaveLength(0) + }) + + it("Should not create a group manually if the admin doesn't exist", async () => { + const fun = groupsService.createGroupManually(groupDto, "wrong") + + await expect(fun).rejects.toThrow(`You are not an admin`) + }) + }) + + describe("# createGroupsManually", () => { + const groupsDtos: Array = [ + { + id: "1", + name: "Group1", + description: "This is a new group1", + treeDepth: 16, + fingerprintDuration: 3600 + }, + { + id: "2", + name: "Group2", + description: "This is a new group2", + treeDepth: 32, + fingerprintDuration: 7200 + } + ] + let admin: Admin + + beforeAll(async () => { + admin = await adminsService.create({ + id: "admin", + address: "0x" + }) + + admin = await adminsService.findOne({ id: admin.id }) + }) + + it("Should create a group manually", async () => { + const groups = await groupsService.createGroupsManually( + groupsDtos, + admin.id + ) + + groups.forEach((group: Group, i: number) => { + expect(group.id).toBe(groupsDtos[i].id) + expect(group.adminId).toBe(admin.id) + expect(group.description).toBe(groupsDtos[i].description) + expect(group.name).toBe(groupsDtos[i].name) + expect(group.treeDepth).toBe(groupsDtos[i].treeDepth) + expect(group.fingerprintDuration).toBe( + groupsDtos[i].fingerprintDuration + ) + expect(group.members).toHaveLength(0) + }) + }) + + it("Should not create a group manually if the admin doesn't exist", async () => { + const fun = groupsService.createGroupsManually(groupsDtos, "wrong") + + await expect(fun).rejects.toThrow(`You are not an admin`) + }) + }) + + describe("# removeGroupManually", () => { + const groupDto: CreateGroupDto = { + id: "1", + name: "Group1", + description: "This is a new group1", + treeDepth: 16, + fingerprintDuration: 3600 + } + let admin: Admin + let group: Group + + beforeAll(async () => { + admin = await adminsService.create({ + id: "admin", + address: "0x" + }) + + admin = await adminsService.findOne({ id: admin.id }) + + group = await groupsService.createGroupManually(groupDto, admin.id) + }) + + it("Should remove a group manually", async () => { + await groupsService.removeGroupManually(group.id, admin.id) + + const fun = groupsService.getGroup(group.id) + + await expect(fun).rejects.toThrow( + `Group with id '${group.id}' does not exist` + ) + }) + + it("Should not remove a group manually if the group doesn't exist", async () => { + const fun = groupsService.removeGroupManually(group.id, admin.id) + + await expect(fun).rejects.toThrow( + `Group with id '${group.id}' does not exist` + ) + }) + + it("Should not remove a group manually if the admin doesn't exist or is not the admin of the group", async () => { + group = await groupsService.createGroupManually(groupDto, admin.id) + + const fun = groupsService.removeGroupManually(group.id, "wrong") + + await expect(fun).rejects.toThrow( + `You are not the admin of the group '${group.id}'` + ) + }) + }) + + describe("# removeGroupsManually", () => { + const groupsDtos: Array = [ + { + id: "1", + name: "Group1", + description: "This is a new group1", + treeDepth: 16, + fingerprintDuration: 3600 + }, + { + id: "2", + name: "Group2", + description: "This is a new group2", + treeDepth: 32, + fingerprintDuration: 7200 + } + ] + let admin: Admin + let groups: Array + let groupId1: string + let groupId2: string + + beforeAll(async () => { + admin = await adminsService.create({ + id: "admin", + address: "0x" + }) + + admin = await adminsService.findOne({ id: admin.id }) + + groups = await groupsService.createGroupsManually( + groupsDtos, + admin.id + ) + + groupId1 = groups[0].id + groupId2 = groups[1].id + }) + + it("Should remove the groups manually", async () => { + await groupsService.removeGroupsManually( + [groupId1, groupId2], + admin.id + ) + + const fun1 = groupsService.getGroup(groupId1) + const fun2 = groupsService.getGroup(groupId2) + + await expect(fun1).rejects.toThrow( + `Group with id '${groupId1}' does not exist` + ) + await expect(fun2).rejects.toThrow( + `Group with id '${groupId2}' does not exist` + ) + }) + + it("Should not remove the groups manually if one or more group doesn't exist", async () => { + const fun = groupsService.removeGroupsManually( + [groupId1, groupId2], + admin.id + ) + + await expect(fun).rejects.toThrow( + `Group with id '${groupId1}' does not exist` + ) + }) + + it("Should not remove the groups manually if the admin doesn't exist or is not the admin of the group", async () => { + groups = await groupsService.createGroupsManually( + groupsDtos, + admin.id + ) + + const fun = groupsService.removeGroupsManually( + [groupId1, groupId2], + "wrong" + ) + + await expect(fun).rejects.toThrow( + `You are not the admin of the group '${groupId1}'` + ) + }) + }) + + describe("# updateGroupManually", () => { + const groupDto: CreateGroupDto = { + id: "1", + name: "Group1", + description: "This is a new group1", + treeDepth: 16, + fingerprintDuration: 3600 + } + + const updateDto: UpdateGroupDto = { + description: "This is a new new group1", + treeDepth: 32, + fingerprintDuration: 7200 + } + let admin: Admin + let group: Group + + beforeAll(async () => { + admin = await adminsService.create({ + id: "admin", + address: "0x" + }) + + admin = await adminsService.findOne({ id: admin.id }) + group = await groupsService.createGroupManually(groupDto, admin.id) + }) + + it("Should update a group manually", async () => { + const updatedGroup = await groupsService.updateGroupManually( + group.id, + updateDto, + admin.id + ) + + expect(updatedGroup.id).toBe(groupDto.id) + expect(updatedGroup.adminId).toBe(admin.id) + expect(updatedGroup.description).toBe(updateDto.description) + expect(updatedGroup.name).toBe(groupDto.name) + expect(updatedGroup.treeDepth).toBe(updateDto.treeDepth) + expect(updatedGroup.fingerprintDuration).toBe( + updateDto.fingerprintDuration + ) + expect(updatedGroup.members).toHaveLength(0) + expect(updatedGroup.credentials).toBeNull() + }) + + it("Should not update a group manually if the group doesn't exist", async () => { + const fun = groupsService.updateGroupManually( + "wrong", + updateDto, + admin.id + ) + + await expect(fun).rejects.toThrow( + `Group with id 'wrong' does not exist` + ) + }) + + it("Should not update a group manually if the admin doesn't exist or is not the admin of the group", async () => { + const fun = groupsService.updateGroupManually( + group.id, + updateDto, + "wrong" + ) + + await expect(fun).rejects.toThrow( + `You are not the admin of the group '${group.id}'` + ) + }) + }) + + describe("# updateGroupsManually", () => { + const groupsDtos: Array = [ + { + id: "1", + name: "Group1", + description: "This is a new group1", + treeDepth: 16, + fingerprintDuration: 3600 + }, + { + id: "2", + name: "Group2", + description: "This is a new group2", + treeDepth: 32, + fingerprintDuration: 7200 + } + ] + + const updateDtos: Array = [ + { + description: "This is a new new group1", + treeDepth: 32, + fingerprintDuration: 7200 + }, + { + description: "This is a new new group2", + treeDepth: 32, + fingerprintDuration: 14400 + } + ] + let admin: Admin + let groups: Array + let groupId1: string + let groupId2: string + + beforeAll(async () => { + admin = await adminsService.create({ + id: "admin", + address: "0x" + }) + + admin = await adminsService.findOne({ id: admin.id }) + groups = await groupsService.createGroupsManually( + groupsDtos, + admin.id + ) + groupId1 = groups[0].id + groupId2 = groups[1].id + }) + + it("Should update the groups manually", async () => { + const updatedGroups = await groupsService.updateGroupsManually( + [groupId1, groupId2], + updateDtos, + admin.id + ) + + updatedGroups.forEach((updatedGroup: Group, i: number) => { + expect(updatedGroup.id).toBe(groupsDtos[i].id) + expect(updatedGroup.adminId).toBe(admin.id) + expect(updatedGroup.description).toBe(updateDtos[i].description) + expect(updatedGroup.name).toBe(groupsDtos[i].name) + expect(updatedGroup.treeDepth).toBe(updateDtos[i].treeDepth) + expect(updatedGroup.fingerprintDuration).toBe( + updateDtos[i].fingerprintDuration + ) + expect(updatedGroup.members).toHaveLength(0) + }) + }) + + it("Should not update the groups manually if one or more groups doesn't exist", async () => { + const fun = groupsService.updateGroupsManually( + ["wrong"], + updateDtos, + admin.id + ) + + await expect(fun).rejects.toThrow( + `Group with id 'wrong' does not exist` + ) + }) + + it("Should not update the groups manually if the admin doesn't exist or is not the admin of the groups", async () => { + const fun = groupsService.updateGroupsManually( + [groupId1, groupId2], + updateDtos, + "wrong" + ) + + await expect(fun).rejects.toThrow( + `You are not the admin of the group '${groupId1}'` + ) + }) + }) + describe("# initialize", () => { it("Should initialize the cached groups", async () => { const currentCachedGroups = await groupsService.getGroups() diff --git a/apps/api/src/app/groups/groups.service.ts b/apps/api/src/app/groups/groups.service.ts index 4d0e60a9..aedd530c 100644 --- a/apps/api/src/app/groups/groups.service.ts +++ b/apps/api/src/app/groups/groups.service.ts @@ -19,7 +19,7 @@ import { UpdateGroupDto } from "./dto/update-group.dto" import { Group } from "./entities/group.entity" import { Member } from "./entities/member.entity" import { MerkleProof } from "./types" -import { adminApiKeyCheck } from "./groups.utils" +import { getAndCheckAdmin } from "./groups.utils" @Injectable() export class GroupsService { @@ -61,16 +61,14 @@ export class GroupsService { /** * Create a group using API Key. * @param dto External parameters used to create a new group. - * @param adminId Admin id. * @param apiKey The API Key. * @returns Created group. */ async createGroupWithAPIKey( dto: CreateGroupDto, - adminId: string, apiKey: string ): Promise { - const groups = await this.createGroupsWithAPIKey([dto], adminId, apiKey) + const groups = await this.createGroupsWithAPIKey([dto], apiKey) return groups.at(0) } @@ -78,20 +76,58 @@ export class GroupsService { /** * Create groups using API Key. * @param dtos External parameters used to create new groups. - * @param adminId Admin id. * @param apiKey The API Key. * @returns Created groups. */ async createGroupsWithAPIKey( dtos: Array, - adminId: string, apiKey: string ): Promise> { const newGroups: Array = [] + const admin = await getAndCheckAdmin(this.adminsService, apiKey) + + for await (const dto of dtos) { + const group = await this.createGroup(dto, admin.id) + + newGroups.push(group) + } + + return newGroups + } + + /** + * Create group manually without using API Key. + * @param dto External parameters used to create a new group. + * @param adminId Admin id. + * @returns Created group. + */ + async createGroupManually( + dto: CreateGroupDto, + adminId: string + ): Promise { + const admin = await this.adminsService.findOne({ id: adminId }) + + if (!admin) throw new BadRequestException(`You are not an admin`) + + return this.createGroup(dto, adminId) + } + + /** + * Create groups manually without using API Key. + * @param dtos External parameters used to create new groups. + * @param adminId Admin id. + * @returns Created groups. + */ + async createGroupsManually( + dtos: Array, + adminId: string + ): Promise> { const admin = await this.adminsService.findOne({ id: adminId }) - await adminApiKeyCheck(admin, apiKey) + if (!admin) throw new BadRequestException(`You are not an admin`) + + const newGroups: Array = [] for await (const dto of dtos) { const group = await this.createGroup(dto, adminId) @@ -160,27 +196,45 @@ export class GroupsService { */ async removeGroupWithAPIKey( groupId: string, - adminId: string, apiKey: string ): Promise { - return this.removeGroupsWithAPIKey([groupId], adminId, apiKey) + return this.removeGroupsWithAPIKey([groupId], apiKey) } /** * Remove groups using API Key. * @param groupsIds Groups identifiers. - * @param adminId Admin id. * @param apiKey the api key. */ async removeGroupsWithAPIKey( groupsIds: Array, - adminId: string, apiKey: string ): Promise { - const admin = await this.adminsService.findOne({ id: adminId }) + const admin = await getAndCheckAdmin(this.adminsService, apiKey) - await adminApiKeyCheck(admin, apiKey) + for await (const groupId of groupsIds) { + await this.removeGroup(groupId, admin.id) + } + } + + /** + * Remove a group manually without using API Key. + * @param groupId Group id. + * @param adminId Admin id. + */ + async removeGroupManually(groupId: string, adminId: string): Promise { + return this.removeGroup(groupId, adminId) + } + /** + * Remove groups manually without using API Key. + * @param groupsIds Groups identifiers. + * @param adminId Admin id. + */ + async removeGroupsManually( + groupsIds: Array, + adminId: string + ): Promise { for await (const groupId of groupsIds) { await this.removeGroup(groupId, adminId) } @@ -194,6 +248,11 @@ export class GroupsService { async removeGroup(groupId: string, adminId: string): Promise { const group = await this.getGroup(groupId) + if (!group) + throw new BadRequestException( + `The group '${groupId}' does not exists` + ) + if (group.adminId !== adminId) { throw new UnauthorizedException( `You are not the admin of the group '${groupId}'` @@ -211,23 +270,82 @@ export class GroupsService { * Update a group using API Key. * @param groupId Group id. * @param dto External parameters used to update a group. - * @param adminId Group admin id. * @param apiKey the API Key. * @returns Updated group. */ async updateGroupWithApiKey( groupId: string, dto: UpdateGroupDto, - adminId: string, apiKey: string ): Promise { - const admin = await this.adminsService.findOne({ id: adminId }) + const admin = await getAndCheckAdmin(this.adminsService, apiKey) + + return this.updateGroup(groupId, dto, admin.id) + } + + /** + * Update groups using API Key. + * @param groupsIds Groups ids. + * @param dtos External parameters used to update groups. + * @param apiKey the API Key. + * @returns Updated group. + */ + async updateGroupsWithApiKey( + groupsIds: Array, + dtos: Array, + apiKey: string + ): Promise> { + const updatedGroups: Array = [] + + const admin = await getAndCheckAdmin(this.adminsService, apiKey) + + for await (const [index, groupId] of groupsIds.entries()) { + const dto = dtos[index] + const group = await this.updateGroup(groupId, dto, admin.id) + updatedGroups.push(group) + } - await adminApiKeyCheck(admin, apiKey, groupId) + return updatedGroups + } + /** + * Update a group manually without using API Key. + * @param groupId Group id. + * @param dto External parameters used to update a group. + * @param adminId Group admin id. + * @returns Updated group. + */ + async updateGroupManually( + groupId: string, + dto: UpdateGroupDto, + adminId: string + ): Promise { return this.updateGroup(groupId, dto, adminId) } + /** + * Update groups manually without using API Key. + * @param groupsIds Groups ids. + * @param dtos External parameters used to update groups. + * @param adminId Group admin id. + * @returns Updated groups. + */ + async updateGroupsManually( + groupsIds: Array, + dtos: Array, + adminId: string + ): Promise> { + const updatedGroups: Array = [] + + for await (const [index, groupId] of groupsIds.entries()) { + const dto = dtos[index] + const group = await this.updateGroup(groupId, dto, adminId) + updatedGroups.push(group) + } + + return updatedGroups + } + /** * Updates some parameters of the group. * @param groupId Group id. @@ -247,6 +365,11 @@ export class GroupsService { ): Promise { const group = await this.getGroup(groupId) + if (!group) + throw new BadRequestException( + `The group '${groupId}' does not exists` + ) + if (group.adminId !== adminId) { throw new UnauthorizedException( `You are not the admin of the group '${groupId}'` diff --git a/apps/api/src/app/groups/groups.utils.test.ts b/apps/api/src/app/groups/groups.utils.test.ts index 12a369ed..a764a298 100644 --- a/apps/api/src/app/groups/groups.utils.test.ts +++ b/apps/api/src/app/groups/groups.utils.test.ts @@ -1,6 +1,47 @@ -import { mapGroupToResponseDTO } from "./groups.utils" +import { ScheduleModule } from "@nestjs/schedule" +import { Test } from "@nestjs/testing" +import { TypeOrmModule } from "@nestjs/typeorm" +import { ApiKeyActions } from "@bandada/utils" +import { Invite } from "../invites/entities/invite.entity" +import { InvitesService } from "../invites/invites.service" +import { OAuthAccount } from "../credentials/entities/credentials-account.entity" +import { Group } from "./entities/group.entity" +import { Member } from "./entities/member.entity" +import { GroupsService } from "./groups.service" +import { AdminsService } from "../admins/admins.service" +import { AdminsModule } from "../admins/admins.module" +import { Admin } from "../admins/entities/admin.entity" +import { mapGroupToResponseDTO, getAndCheckAdmin } from "./groups.utils" describe("Groups utils", () => { + let groupsService: GroupsService + let adminsService: AdminsService + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [ + TypeOrmModule.forRootAsync({ + useFactory: () => ({ + type: "sqlite", + database: ":memory:", + dropSchema: true, + entities: [Group, Invite, Member, OAuthAccount, Admin], + synchronize: true + }) + }), + TypeOrmModule.forFeature([Group, Invite, Member, Admin]), + ScheduleModule.forRoot(), + AdminsModule + ], + providers: [GroupsService, InvitesService, AdminsService] + }).compile() + + groupsService = await module.resolve(GroupsService) + adminsService = await module.resolve(AdminsService) + + await groupsService.initialize() + }) + describe("# mapGroupToResponseDTO", () => { it("Should map the group data", async () => { const group = { @@ -29,4 +70,68 @@ describe("Groups utils", () => { expect(fingerprint).toBe("12345") }) }) + + describe("# getAndCheckAdmin", () => { + const groupId = "1" + let apiKey = "" + let admin: Admin = {} as any + + beforeAll(async () => { + admin = await adminsService.create({ + id: groupId, + address: "0x00" + }) + + apiKey = await adminsService.updateApiKey( + admin.id, + ApiKeyActions.Generate + ) + + admin = await adminsService.findOne({ id: admin.id }) + }) + + it("Should successfully check and return the admin", async () => { + const checkedAdmin = await getAndCheckAdmin(adminsService, apiKey) + + expect(checkedAdmin.id).toBe(admin.id) + expect(checkedAdmin.address).toBe(admin.address) + expect(checkedAdmin.apiKey).toBe(admin.apiKey) + expect(checkedAdmin.apiEnabled).toBe(admin.apiEnabled) + expect(checkedAdmin.username).toBe(admin.username) + }) + + it("Should throw if the API Key or admin is invalid", async () => { + const fun = getAndCheckAdmin(adminsService, "wrong") + + await expect(fun).rejects.toThrow( + `Invalid API key or invalid admin for the groups` + ) + }) + + it("Should throw if the API Key or admin is invalid (w/ group identifier)", async () => { + const fun = getAndCheckAdmin(adminsService, "wrong", groupId) + + await expect(fun).rejects.toThrow( + `Invalid API key or invalid admin for the group '${groupId}'` + ) + }) + + it("Should throw if the API Key is invalid or API access is disabled", async () => { + await adminsService.updateApiKey(admin.id, ApiKeyActions.Disable) + + const fun = getAndCheckAdmin(adminsService, apiKey) + + await expect(fun).rejects.toThrow( + `Invalid API key or API access not enabled for admin '${admin.id}'` + ) + }) + + it("Should throw if the API Key is invalid or API access is disabled (w/ group identifier)", async () => { + const fun = getAndCheckAdmin(adminsService, apiKey, groupId) + + await expect(fun).rejects.toThrow( + `Invalid API key or API access not enabled for admin '${admin.id}'` + ) + }) + }) }) diff --git a/apps/api/src/app/groups/groups.utils.ts b/apps/api/src/app/groups/groups.utils.ts index a85764aa..2bc95e7c 100644 --- a/apps/api/src/app/groups/groups.utils.ts +++ b/apps/api/src/app/groups/groups.utils.ts @@ -1,6 +1,7 @@ import { BadRequestException } from "@nestjs/common" import { Group } from "./entities/group.entity" import { Admin } from "../admins/entities/admin.entity" +import { AdminsService } from "../admins/admins.service" export function mapGroupToResponseDTO(group: Group, fingerprint: string = "") { const dto = { @@ -19,16 +20,18 @@ export function mapGroupToResponseDTO(group: Group, fingerprint: string = "") { return dto } -export async function adminApiKeyCheck( - admin: Admin, +export async function getAndCheckAdmin( + adminService: AdminsService, apiKey: string, groupId?: string -) { - if (!admin) { +): Promise { + const admin = await adminService.findOne({ apiKey }) + + if (!apiKey || !admin) { throw new BadRequestException( groupId - ? `Invalid admin for the group '${groupId}'` - : `Invalid admin for the groups` + ? `Invalid API key or invalid admin for the group '${groupId}'` + : `Invalid API key or invalid admin for the groups` ) } @@ -37,4 +40,6 @@ export async function adminApiKeyCheck( `Invalid API key or API access not enabled for admin '${admin.id}'` ) } + + return admin }