diff --git a/apps/api/src/app/groups/groups.service.ts b/apps/api/src/app/groups/groups.service.ts index 509eb32..0628375 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 { getAndCheckAdmin } from "./groups.utils" +import { getAndCheckAdmin } from "../utils" @Injectable() export class GroupsService { diff --git a/apps/api/src/app/groups/groups.utils.test.ts b/apps/api/src/app/groups/groups.utils.test.ts index a764a29..7e4034e 100644 --- a/apps/api/src/app/groups/groups.utils.test.ts +++ b/apps/api/src/app/groups/groups.utils.test.ts @@ -1,7 +1,6 @@ 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" @@ -11,11 +10,10 @@ 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" +import { mapGroupToResponseDTO } from "./groups.utils" describe("Groups utils", () => { let groupsService: GroupsService - let adminsService: AdminsService beforeAll(async () => { const module = await Test.createTestingModule({ @@ -37,7 +35,6 @@ describe("Groups utils", () => { }).compile() groupsService = await module.resolve(GroupsService) - adminsService = await module.resolve(AdminsService) await groupsService.initialize() }) @@ -70,68 +67,4 @@ 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 2bc95e7..5c56e51 100644 --- a/apps/api/src/app/groups/groups.utils.ts +++ b/apps/api/src/app/groups/groups.utils.ts @@ -1,7 +1,4 @@ -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,27 +16,3 @@ export function mapGroupToResponseDTO(group: Group, fingerprint: string = "") { return dto } - -export async function getAndCheckAdmin( - adminService: AdminsService, - apiKey: string, - groupId?: string -): Promise { - const admin = await adminService.findOne({ apiKey }) - - if (!apiKey || !admin) { - throw new BadRequestException( - groupId - ? `Invalid API key or invalid admin for the group '${groupId}'` - : `Invalid API key or invalid admin for the groups` - ) - } - - if (!admin.apiEnabled || admin.apiKey !== apiKey) { - throw new BadRequestException( - `Invalid API key or API access not enabled for admin '${admin.id}'` - ) - } - - return admin -} diff --git a/apps/api/src/app/invites/invites.controller.ts b/apps/api/src/app/invites/invites.controller.ts index fddedf7..9ddd800 100644 --- a/apps/api/src/app/invites/invites.controller.ts +++ b/apps/api/src/app/invites/invites.controller.ts @@ -2,14 +2,17 @@ import { Body, Controller, Get, + Headers, + NotImplementedException, Param, Post, Req, UseGuards } from "@nestjs/common" import { + ApiBody, ApiCreatedResponse, - ApiExcludeEndpoint, + ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger" @@ -30,17 +33,36 @@ export class InvitesController { @Post() @UseGuards(AuthGuard) @UseGuards(ThrottlerGuard) - @ApiExcludeEndpoint() + @ApiBody({ type: CreateInviteDto }) + @ApiHeader({ name: "x-api-key", required: true }) + @ApiCreatedResponse({ type: Invite }) + @ApiOperation({ + description: "Creates a new group invite with a unique code." + }) async createInvite( + @Headers() headers: Headers, @Req() req: Request, @Body() dto: CreateInviteDto - ): Promise { - const { code } = await this.invitesService.createInvite( - dto, - req.session.adminId - ) + ): Promise { + let invite: Invite - return code + const apiKey = headers["x-api-key"] as string + + if (apiKey) { + invite = await this.invitesService.createInviteWithApiKey( + dto, + apiKey + ) + } else if (req.session.adminId) { + invite = await this.invitesService.createInviteManually( + dto, + req.session.adminId + ) + } else { + throw new NotImplementedException() + } + + return invite } @Get(":code") diff --git a/apps/api/src/app/invites/invites.module.ts b/apps/api/src/app/invites/invites.module.ts index a3eba97..c793b3b 100644 --- a/apps/api/src/app/invites/invites.module.ts +++ b/apps/api/src/app/invites/invites.module.ts @@ -5,15 +5,17 @@ import { Invite } from "./entities/invite.entity" import { InvitesController } from "./invites.controller" import { InvitesService } from "./invites.service" import { AdminsModule } from "../admins/admins.module" +import { AdminsService } from "../admins/admins.service" +import { Admin } from "../admins/entities/admin.entity" @Module({ imports: [ forwardRef(() => GroupsModule), - TypeOrmModule.forFeature([Invite]), + TypeOrmModule.forFeature([Invite, Admin]), AdminsModule ], controllers: [InvitesController], - providers: [InvitesService], + providers: [InvitesService, AdminsService], exports: [InvitesService] }) export class InvitesModule {} diff --git a/apps/api/src/app/invites/invites.service.test.ts b/apps/api/src/app/invites/invites.service.test.ts index 8aa4c45..b9513c8 100644 --- a/apps/api/src/app/invites/invites.service.test.ts +++ b/apps/api/src/app/invites/invites.service.test.ts @@ -1,6 +1,7 @@ import { ScheduleModule } from "@nestjs/schedule" import { Test } from "@nestjs/testing" import { TypeOrmModule } from "@nestjs/typeorm" +import { ApiKeyActions } from "@bandada/utils" import { Group } from "../groups/entities/group.entity" import { Member } from "../groups/entities/member.entity" import { GroupsService } from "../groups/groups.service" @@ -8,6 +9,8 @@ import { OAuthAccount } from "../credentials/entities/credentials-account.entity import { Invite } from "./entities/invite.entity" import { InvitesService } from "./invites.service" import { AdminsModule } from "../admins/admins.module" +import { AdminsService } from "../admins/admins.service" +import { Admin } from "../admins/entities/admin.entity" jest.mock("@bandada/utils", () => { const originalModule = jest.requireActual("@bandada/utils") @@ -28,7 +31,9 @@ jest.mock("@bandada/utils", () => { describe("InvitesService", () => { let invitesService: InvitesService let groupsService: GroupsService + let adminsService: AdminsService let groupId: string + let admin: Admin beforeAll(async () => { const module = await Test.createTestingModule({ @@ -38,19 +43,29 @@ describe("InvitesService", () => { type: "sqlite", database: ":memory:", dropSchema: true, - entities: [Group, Invite, Member, OAuthAccount], + entities: [Group, Invite, Member, OAuthAccount, Admin], synchronize: true }) }), - TypeOrmModule.forFeature([Group, Invite, Member]), + TypeOrmModule.forFeature([Group, Invite, Member, Admin]), ScheduleModule.forRoot(), AdminsModule ], - providers: [GroupsService, InvitesService] + providers: [GroupsService, InvitesService, AdminsService] }).compile() invitesService = await module.resolve(InvitesService) groupsService = await module.resolve(GroupsService) + adminsService = await module.resolve(AdminsService) + + admin = await adminsService.create({ + id: "admin", + address: "0x" + }) + + await adminsService.updateApiKey(admin.id, ApiKeyActions.Generate) + + admin = await adminsService.findOne({ id: admin.id }) const group = await groupsService.createGroup( { @@ -59,7 +74,7 @@ describe("InvitesService", () => { treeDepth: 16, fingerprintDuration: 3600 }, - "admin" + admin.id ) groupId = group.id @@ -71,7 +86,7 @@ describe("InvitesService", () => { group, code, isRedeemed: redeemed - } = await invitesService.createInvite({ groupId }, "admin") + } = await invitesService.createInvite({ groupId }, admin.id) expect(redeemed).toBeFalsy() expect(code).toHaveLength(8) @@ -98,12 +113,225 @@ describe("InvitesService", () => { } } }, - "admin" + admin.id ) const fun = invitesService.createInvite( { groupId: group.id }, - "admin" + admin.id + ) + + await expect(fun).rejects.toThrow( + "Credential groups cannot be accessed via invites" + ) + }) + }) + + describe("# createInviteManually", () => { + it("Should create an invite manually", async () => { + const { + group, + code, + isRedeemed: redeemed + } = await invitesService.createInviteManually({ groupId }, admin.id) + + expect(redeemed).toBeFalsy() + expect(code).toHaveLength(8) + expect(group.treeDepth).toBe(16) + }) + + it("Should not create an invite if the given identifier does not belong to an admin", async () => { + const fun = invitesService.createInviteManually( + { groupId }, + "wrong-admin" + ) + + await expect(fun).rejects.toThrow("You are not an admin") + }) + + it("Should not create an invite if the admin is the wrong one", async () => { + const admin2 = await adminsService.create({ + id: "admin2", + address: "0x02" + }) + + await groupsService.createGroup( + { + name: "Group2", + description: "This is a description", + treeDepth: 16, + fingerprintDuration: 3600, + credentials: { + id: "GITHUB_FOLLOWERS", + criteria: { + minFollowers: 12 + } + } + }, + admin2.id + ) + + const fun = invitesService.createInviteManually( + { groupId }, + admin2.id + ) + + await expect(fun).rejects.toThrow("You are not the admin") + }) + + it("Should not create an invite if the group is a credential group", async () => { + const admin3 = await adminsService.create({ + id: "admin3", + address: "0x04" + }) + + const group = await groupsService.createGroup( + { + name: "Group3", + description: "This is a description", + treeDepth: 16, + fingerprintDuration: 3600, + credentials: { + id: "GITHUB_FOLLOWERS", + criteria: { + minFollowers: 12 + } + } + }, + admin3.id + ) + + const fun = invitesService.createInviteManually( + { groupId: group.id }, + admin3.id + ) + + await expect(fun).rejects.toThrow( + "Credential groups cannot be accessed via invites" + ) + }) + }) + + describe("# createInviteWithApiKey", () => { + it("Should create an invite manually", async () => { + const { + group, + code, + isRedeemed: redeemed + } = await invitesService.createInviteWithApiKey( + { groupId }, + admin.apiKey + ) + + expect(redeemed).toBeFalsy() + expect(code).toHaveLength(8) + expect(group.treeDepth).toBe(16) + }) + + it("Should not create an invite if the given api key is invalid", async () => { + const fun = invitesService.createInviteWithApiKey( + { groupId }, + "wrong-apikey" + ) + + await expect(fun).rejects.toThrow( + `Invalid API key or invalid admin for the group '${groupId}'` + ) + }) + + it("Should not create an invite if the given api key does not belong to an admin", async () => { + const oldApiKey = admin.apiKey + + await adminsService.updateApiKey(admin.id, ApiKeyActions.Generate) + + const fun = invitesService.createInviteWithApiKey( + { groupId }, + oldApiKey + ) + + await expect(fun).rejects.toThrow( + `Invalid API key or invalid admin for the group '${groupId}'` + ) + }) + + it("Should not create an invite if the given api key is disabled", async () => { + await adminsService.updateApiKey(admin.id, ApiKeyActions.Disable) + + admin = await adminsService.findOne({ id: admin.id }) + + const fun = invitesService.createInviteWithApiKey( + { groupId }, + admin.apiKey + ) + + await expect(fun).rejects.toThrow( + `Invalid API key or API access not enabled for admin '${admin.id}'` + ) + }) + + it("Should not create an invite if the admin is the wrong one", async () => { + let admin2 = await adminsService.create({ + id: "admin2", + address: "0x02" + }) + + await adminsService.updateApiKey(admin2.id, ApiKeyActions.Generate) + + admin2 = await adminsService.findOne({ id: admin2.id }) + + await groupsService.createGroup( + { + name: "Group2", + description: "This is a description", + treeDepth: 16, + fingerprintDuration: 3600, + credentials: { + id: "GITHUB_FOLLOWERS", + criteria: { + minFollowers: 12 + } + } + }, + admin2.id + ) + + const fun = invitesService.createInviteWithApiKey( + { groupId }, + admin2.apiKey + ) + + await expect(fun).rejects.toThrow("You are not the admin") + }) + + it("Should not create an invite if the group is a credential group", async () => { + let admin3 = await adminsService.create({ + id: "admin3", + address: "0x04" + }) + + await adminsService.updateApiKey(admin3.id, ApiKeyActions.Generate) + + admin3 = await adminsService.findOne({ id: admin3.id }) + + const group = await groupsService.createGroup( + { + name: "Group3", + description: "This is a description", + treeDepth: 16, + fingerprintDuration: 3600, + credentials: { + id: "GITHUB_FOLLOWERS", + criteria: { + minFollowers: 12 + } + } + }, + admin3.id + ) + + const fun = invitesService.createInviteWithApiKey( + { groupId: group.id }, + admin3.apiKey ) await expect(fun).rejects.toThrow( @@ -116,7 +344,7 @@ describe("InvitesService", () => { it("Should get an invite", async () => { const { code } = await invitesService.createInvite( { groupId }, - "admin" + admin.id ) const invite = await invitesService.getInvite(code) @@ -137,7 +365,7 @@ describe("InvitesService", () => { let invite: Invite beforeAll(async () => { - invite = await invitesService.createInvite({ groupId }, "admin") + invite = await invitesService.createInvite({ groupId }, admin.id) }) it("Should not redeem an invite if group name does not match", async () => { diff --git a/apps/api/src/app/invites/invites.service.ts b/apps/api/src/app/invites/invites.service.ts index 937ec80..afd6d4b 100644 --- a/apps/api/src/app/invites/invites.service.ts +++ b/apps/api/src/app/invites/invites.service.ts @@ -11,6 +11,8 @@ import { Repository } from "typeorm" import { GroupsService } from "../groups/groups.service" import { CreateInviteDto } from "./dto/create-invite.dto" import { Invite } from "./entities/invite.entity" +import { getAndCheckAdmin } from "../utils" +import { AdminsService } from "../admins/admins.service" @Injectable() export class InvitesService { @@ -18,9 +20,46 @@ export class InvitesService { @InjectRepository(Invite) private readonly inviteRepository: Repository, @Inject(forwardRef(() => GroupsService)) - private readonly groupsService: GroupsService + private readonly groupsService: GroupsService, + private readonly adminsService: AdminsService ) {} + /** + * Create a new group invite using API Key. + * @param dto External parameters used to create a new group invite. + * @param apiKey the API Key. + * @returns The group invite. + */ + async createInviteWithApiKey( + dto: CreateInviteDto, + apiKey: string + ): Promise { + const admin = await getAndCheckAdmin( + this.adminsService, + apiKey, + dto.groupId + ) + + return this.createInvite(dto, admin.id) + } + + /** + * Create a new group invite manually without using API Key. + * @param dto External parameters used to create a new group invite. + * @param adminId Group admin id. + * @returns The group invite. + */ + async createInviteManually( + dto: CreateInviteDto, + adminId: string + ): Promise { + const admin = await this.adminsService.findOne({ id: adminId }) + + if (!admin) throw new BadRequestException(`You are not an admin`) + + return this.createInvite(dto, adminId) + } + /** * Creates a new group invite with a unique code. Group invites can only be * created by group admins. diff --git a/apps/api/src/app/utils/getAndCheckAdmin.ts b/apps/api/src/app/utils/getAndCheckAdmin.ts new file mode 100644 index 0000000..43e0f5e --- /dev/null +++ b/apps/api/src/app/utils/getAndCheckAdmin.ts @@ -0,0 +1,27 @@ +import { BadRequestException } from "@nestjs/common" +import { AdminsService } from "../admins/admins.service" +import { Admin } from "../admins/entities/admin.entity" + +export default async function getAndCheckAdmin( + adminService: AdminsService, + apiKey: string, + groupId?: string +): Promise { + const admin = await adminService.findOne({ apiKey }) + + if (!apiKey || !admin) { + throw new BadRequestException( + groupId + ? `Invalid API key or invalid admin for the group '${groupId}'` + : `Invalid API key or invalid admin for the groups` + ) + } + + if (!admin.apiEnabled || admin.apiKey !== apiKey) { + throw new BadRequestException( + `Invalid API key or API access not enabled for admin '${admin.id}'` + ) + } + + return admin +} diff --git a/apps/api/src/app/utils/index.test.ts b/apps/api/src/app/utils/index.test.ts index 76016f5..edd8bd5 100644 --- a/apps/api/src/app/utils/index.test.ts +++ b/apps/api/src/app/utils/index.test.ts @@ -1,7 +1,49 @@ +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 { AdminsService } from "../admins/admins.service" +import { AdminsModule } from "../admins/admins.module" +import { Admin } from "../admins/entities/admin.entity" +import { GroupsService } from "../groups/groups.service" +import { Group } from "../groups/entities/group.entity" +import { Member } from "../groups/entities/member.entity" import mapEntity from "./mapEntity" import stringifyJSON from "./stringifyJSON" +import getAndCheckAdmin from "./getAndCheckAdmin" describe("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("# mapEntity", () => { it("Should map a DB entity", async () => { const entity = mapEntity({ id: 1, a: 2 }) as any @@ -18,4 +60,68 @@ describe("Utils", () => { expect(entity.a).toBe("143234") }) }) + + 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/utils/index.ts b/apps/api/src/app/utils/index.ts index a2e948c..e6de4bb 100644 --- a/apps/api/src/app/utils/index.ts +++ b/apps/api/src/app/utils/index.ts @@ -1,4 +1,5 @@ import mapEntity from "./mapEntity" import stringifyJSON from "./stringifyJSON" +import getAndCheckAdmin from "./getAndCheckAdmin" -export { mapEntity, stringifyJSON } +export { mapEntity, stringifyJSON, getAndCheckAdmin } diff --git a/apps/client/src/pages/home.tsx b/apps/client/src/pages/home.tsx index a03957a..2dadf86 100644 --- a/apps/client/src/pages/home.tsx +++ b/apps/client/src/pages/home.tsx @@ -73,7 +73,7 @@ export default function HomePage(): JSX.Element { const identityCommitment = identity.getCommitment().toString() const hasJoined = await isGroupMember( - invite.groupId, + invite.group.id, identityCommitment ) @@ -89,7 +89,7 @@ export default function HomePage(): JSX.Element { } const response = await addMemberByInviteCode( - invite.groupId, + invite.group.id, identityCommitment, inviteCode ) diff --git a/apps/dashboard/src/api/bandadaAPI.ts b/apps/dashboard/src/api/bandadaAPI.ts index b53c8f1..dd069c6 100644 --- a/apps/dashboard/src/api/bandadaAPI.ts +++ b/apps/dashboard/src/api/bandadaAPI.ts @@ -20,14 +20,14 @@ export async function generateMagicLink( clientUrl?: string ): Promise { try { - const code = await request(`${API_URL}/invites`, { + const invite = await request(`${API_URL}/invites`, { method: "POST", data: { groupId } }) - return (clientUrl || CLIENT_INVITES_URL).replace("\\", code) + return (clientUrl || CLIENT_INVITES_URL).replace("\\", invite.code) } catch (error: any) { console.error(error) createAlert(error.response.data.message) diff --git a/libs/api-sdk/src/apiSdk.ts b/libs/api-sdk/src/apiSdk.ts index 4ec38b7..989eb1b 100644 --- a/libs/api-sdk/src/apiSdk.ts +++ b/libs/api-sdk/src/apiSdk.ts @@ -22,7 +22,7 @@ import { removeMemberByApiKey, removeMembersByApiKey } from "./groups" -import { getInvite } from "./invites" +import { createInvite, getInvite } from "./invites" export default class ApiSdk { private _url: string @@ -304,13 +304,25 @@ export default class ApiSdk { await removeMembersByApiKey(this._config, groupId, memberIds, apiKey) } + /** + * Creates a new group invite. + * @param groupId The group identifier. + * @param apiKey The api key. + * @returns Specific invite. + */ + async createInvite(groupId: string, apiKey: string): Promise { + const invite = await createInvite(this._config, groupId, apiKey) + + return invite + } + /** * Returns a specific invite. * @param inviteCode Invite code. * @returns Specific invite. */ async getInvite(inviteCode: string): Promise { - const invite = getInvite(this._config, inviteCode) + const invite = await getInvite(this._config, inviteCode) return invite } diff --git a/libs/api-sdk/src/index.test.ts b/libs/api-sdk/src/index.test.ts index 861ae5a..d889443 100644 --- a/libs/api-sdk/src/index.test.ts +++ b/libs/api-sdk/src/index.test.ts @@ -521,10 +521,53 @@ describe("Bandada API SDK", () => { }) }) describe("Invites", () => { + it("# createInvite", async () => { + const groupId = "95633257675970239314311768035433" + const groupName = "Group 1" + const group = { + id: groupId, + name: groupName, + description: "This is Group 1", + adminId: + "0x63229164c457584616006e31d1e171e6cdd4163695bc9c4bf0227095998ffa4c", + treeDepth: 16, + fingerprintDuration: 3600, + credentials: null, + apiEnabled: false, + apiKey: null, + createdAt: "2023-08-09T18:09:53.000Z", + updatedAt: "2023-08-09T18:09:53.000Z" + } + const apiKey = "70f07d0d-6aa2-4fe1-b4b9-06c271a641dc" + const inviteId = 1 + const inviteCode = "C5VAG4HD" + const inviteCreatedAt = "2023-08-09T18:10:02.000Z" + + requestMocked.mockImplementationOnce(() => + Promise.resolve({ + id: inviteId, + code: inviteCode, + isRedeemed: false, + createdAt: inviteCreatedAt, + group + }) + ) + + apiSdk = new ApiSdk(SupportedUrl.DEV) + const invite: Invite = await apiSdk.createInvite(groupId, apiKey) + + expect(invite.id).toBe(inviteId) + expect(invite.code).toBe(inviteCode) + expect(invite.createdAt).toBe(inviteCreatedAt) + expect(invite.code).toBe(inviteCode) + expect(invite.group).toStrictEqual(group) + }) + describe("# getInvite", () => { it("Should return an invite", async () => { requestMocked.mockImplementationOnce(() => Promise.resolve({ + id: 1, code: "C5VAG4HD", isRedeemed: false, createdAt: "2023-08-09T18:10:02.000Z", diff --git a/libs/api-sdk/src/invites.ts b/libs/api-sdk/src/invites.ts index 1292abe..3de7da1 100644 --- a/libs/api-sdk/src/invites.ts +++ b/libs/api-sdk/src/invites.ts @@ -20,3 +20,27 @@ export async function getInvite( return invite } + +/** + * Creates one new group invite. + * @param groupId The group identifier. + * @param apiKey API Key of the admin. + * @returns Invite. + */ +export async function createInvite( + config: object, + groupId: string, + apiKey: string +): Promise { + const newConfig: any = { + method: "post", + data: groupId, + ...config + } + + newConfig.headers["x-api-key"] = apiKey + + const req = await request(url, newConfig) + + return req +} diff --git a/libs/api-sdk/src/types/index.ts b/libs/api-sdk/src/types/index.ts index 5f864e1..1e2f523 100644 --- a/libs/api-sdk/src/types/index.ts +++ b/libs/api-sdk/src/types/index.ts @@ -44,12 +44,11 @@ type GroupSummary = { } export type Invite = { + id: number code: string isRedeemed: boolean - createdAt: Date group: GroupSummary - groupName: string - groupId: string + createdAt: Date } export enum SupportedUrl {