From 41a2797eaccc6d226980180c9e350dc293a589be Mon Sep 17 00:00:00 2001 From: Han <56923450+waddaboo@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:32:30 +0800 Subject: [PATCH 1/9] feat(api): enable redeemInvite in controller --- apps/api/src/app/invites/invites.controller.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/api/src/app/invites/invites.controller.ts b/apps/api/src/app/invites/invites.controller.ts index 65b883e0..d689acb2 100644 --- a/apps/api/src/app/invites/invites.controller.ts +++ b/apps/api/src/app/invites/invites.controller.ts @@ -72,4 +72,19 @@ export class InvitesController { return mapEntity(invite) } + + @Post("redeem/:code/group/:group") + @ApiOperation({ description: "Redeems a specific invite." }) + @ApiCreatedResponse({ type: InviteResponse }) + async redeemInvite( + @Param("code") inviteCode: string, + @Param("group") groupId: string + ): Promise { + const invite = await this.invitesService.redeemInvite( + inviteCode, + groupId + ) + + return mapEntity(invite) + } } From 924b48322c9353123d01c810bf0472900e5c77ae Mon Sep 17 00:00:00 2001 From: Han <56923450+waddaboo@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:46:29 +0800 Subject: [PATCH 2/9] feat(api-sdk): add redeemInvite --- libs/api-sdk/README.md | 13 ++++++++++++ libs/api-sdk/src/apiSdk.ts | 14 +++++++++++- libs/api-sdk/src/index.test.ts | 39 ++++++++++++++++++++++++++++++++++ libs/api-sdk/src/invites.ts | 17 +++++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/libs/api-sdk/README.md b/libs/api-sdk/README.md index 51ed17cf..035ce939 100644 --- a/libs/api-sdk/README.md +++ b/libs/api-sdk/README.md @@ -521,6 +521,19 @@ const apiKey = "70f07d0d-6aa2-4fe1-b4b9-06c271a641dc" const invite = await apiSdk.getInvite(inviteCode) ``` +## Redeem invite + +\# **redeemInvite**(): _Promise\_ + +Redeems a specific invite. + +```ts +const groupId = "10402173435763029700781503965100" +const inviteCode = "C5VAG4HD" + +const invite = await apiSdk.redeemInvite(inviteCode, groupId) +``` + ## Get credential group join URL \# **getCredentialGroupJoinUrl**(): _string_ diff --git a/libs/api-sdk/src/apiSdk.ts b/libs/api-sdk/src/apiSdk.ts index a18ba4ff..d26323ef 100644 --- a/libs/api-sdk/src/apiSdk.ts +++ b/libs/api-sdk/src/apiSdk.ts @@ -31,7 +31,7 @@ import { getGroupsByType, createAssociatedGroup } from "./groups" -import { createInvite, getInvite } from "./invites" +import { createInvite, getInvite, redeemInvite } from "./invites" export default class ApiSdk { private _url: string @@ -419,6 +419,18 @@ export default class ApiSdk { return invite } + /** + * Redeems a specific invite. + * @param inviteCode Invite code. + * @param groupId Group id. + * @returns The updated invite. + */ + async redeemInvite(inviteCode: string, groupId: string): Promise { + const invite = await redeemInvite(this._config, inviteCode, groupId) + + return invite + } + /** * Generate a custom url for joining a credential group. * @param dashboardUrl Dashboard base url. diff --git a/libs/api-sdk/src/index.test.ts b/libs/api-sdk/src/index.test.ts index f48173f3..35d27f0e 100644 --- a/libs/api-sdk/src/index.test.ts +++ b/libs/api-sdk/src/index.test.ts @@ -1151,6 +1151,45 @@ describe("Bandada API SDK", () => { expect(invite.group).toStrictEqual(group) }) }) + + describe("# redeemInvite", () => { + it("Should redeem an invite", async () => { + const groupId = "95633257675970239314311768035433" + const groupName = "Group 1" + const group = { + id: groupId, + name: groupName, + description: "This is Group 1", + type: "off-chain", + 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 inviteCode = "C5VAG4HD" + const inviteCreatedAt = "2023-08-09T18:10:02.000Z" + + requestMocked.mockImplementationOnce(() => + Promise.resolve({ + code: inviteCode, + isRedeemed: true, + createdAt: inviteCreatedAt, + group + }) + ) + + const apiSdk: ApiSdk = new ApiSdk(SupportedUrl.DEV) + const invite = await apiSdk.redeemInvite(inviteCode, groupId) + + expect(invite.code).toBe(inviteCode) + expect(invite.isRedeemed).toBe(true) + }) + }) }) describe("Check Parameter", () => { describe("Should not throw an error if the parameter has the expected type", () => { diff --git a/libs/api-sdk/src/invites.ts b/libs/api-sdk/src/invites.ts index df4af2b7..ba2d4b71 100644 --- a/libs/api-sdk/src/invites.ts +++ b/libs/api-sdk/src/invites.ts @@ -46,3 +46,20 @@ export async function createInvite( return req } + +export async function redeemInvite( + config: object, + inviteCode: string, + groupId: string +): Promise { + const requestUrl = `${url}/redeem/${inviteCode}/group/${groupId}` + + const newConfig: any = { + method: "post", + ...config + } + + const req = await request(requestUrl, newConfig) + + return req +} From 729d3583a265fcbcc3208031cd1cd72415641552 Mon Sep 17 00:00:00 2001 From: Han <56923450+waddaboo@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:47:14 +0800 Subject: [PATCH 3/9] feat(client): add redeemInvite for on-chain join group by invite --- apps/client/src/api/bandadaAPI.ts | 19 +++++++++++++++++++ apps/client/src/pages/home.tsx | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/apps/client/src/api/bandadaAPI.ts b/apps/client/src/api/bandadaAPI.ts index 49aba4d2..661d2391 100644 --- a/apps/client/src/api/bandadaAPI.ts +++ b/apps/client/src/api/bandadaAPI.ts @@ -72,3 +72,22 @@ export async function addMemberByInviteCode( return null } } + +export async function redeemInvite( + inviteCode: string, + groupId: string +): Promise { + try { + return await api.redeemInvite(inviteCode, groupId) + } catch (error: any) { + console.error(error) + + if (error.response) { + alert(error.response.statusText) + } else { + alert("Some error occurred!") + } + + return null + } +} diff --git a/apps/client/src/pages/home.tsx b/apps/client/src/pages/home.tsx index 8305dcbc..f4f5dd19 100644 --- a/apps/client/src/pages/home.tsx +++ b/apps/client/src/pages/home.tsx @@ -118,6 +118,10 @@ export default function HomePage(): JSX.Element { ) await semaphore.addMember(group.id, identityCommitment) + await bandadaAPI.redeemInvite( + inviteCode, + invite.group.id + ) } catch (error) { alert( "Some error occurred! Check if you're on Sepolia network and the transaction is signed and completed" From 82522260ee38a244bc83703695b15761b188c201 Mon Sep 17 00:00:00 2001 From: Han <56923450+waddaboo@users.noreply.github.com> Date: Fri, 1 Nov 2024 20:46:16 +0800 Subject: [PATCH 4/9] feat(api): add redeem invite with api key and manually --- .../src/app/invites/invites.service.test.ts | 114 +++++++++++++++++- apps/api/src/app/invites/invites.service.ts | 37 ++++++ 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/apps/api/src/app/invites/invites.service.test.ts b/apps/api/src/app/invites/invites.service.test.ts index e3ac0a48..865b95e4 100644 --- a/apps/api/src/app/invites/invites.service.test.ts +++ b/apps/api/src/app/invites/invites.service.test.ts @@ -217,7 +217,7 @@ describe("InvitesService", () => { }) describe("# createInviteWithApiKey", () => { - it("Should create an invite manually", async () => { + it("Should create an invite with api key", async () => { const { group, code, @@ -402,6 +402,118 @@ describe("InvitesService", () => { }) }) + describe("# redeemInviteWithApiKey", () => { + it("Should redeem an invite with api key", async () => { + await adminsService.updateApiKey(admin.id, ApiKeyActions.Enable) + + const invite = await invitesService.createInvite( + { groupId }, + admin.id + ) + + const { isRedeemed: redeemed } = + await invitesService.redeemInviteWithApiKey( + invite.code, + groupId, + admin.apiKey + ) + + expect(redeemed).toBeTruthy() + }) + + it("Should not reddem an invite if the given api key is invalid", async () => { + const invite = await invitesService.createInvite( + { groupId }, + admin.id + ) + + const fun = invitesService.redeemInviteWithApiKey( + invite.code, + groupId, + "wrong-apikey" + ) + + await expect(fun).rejects.toThrow( + `Invalid API key or invalid admin for the group '${groupId}'` + ) + }) + + it("Should not redeem an invite if the given api key does not belong to an admin", async () => { + const invite = await invitesService.createInvite( + { groupId }, + admin.id + ) + + const oldApiKey = admin.apiKey + + await adminsService.updateApiKey(admin.id, ApiKeyActions.Generate) + + const fun = invitesService.redeemInviteWithApiKey( + invite.code, + groupId, + oldApiKey + ) + + await expect(fun).rejects.toThrow( + `Invalid API key or invalid admin for the group '${groupId}'` + ) + }) + + it("Should not redeem an invite if the given api key is disabled", async () => { + const invite = await invitesService.createInvite( + { groupId }, + admin.id + ) + + await adminsService.updateApiKey(admin.id, ApiKeyActions.Disable) + + admin = await adminsService.findOne({ id: admin.id }) + + const fun = invitesService.redeemInviteWithApiKey( + invite.code, + groupId, + admin.apiKey + ) + + await expect(fun).rejects.toThrow( + `Invalid API key or API access not enabled for admin '${admin.id}'` + ) + }) + }) + + describe("# redeemInviteKeyManually", () => { + it("Should redeem an invite manually", async () => { + const invite = await invitesService.createInvite( + { groupId }, + admin.id + ) + + const { isRedeemed: redeemed } = + await invitesService.redeemInviteKeyManually( + invite.code, + groupId, + admin.id + ) + + expect(redeemed).toBeTruthy() + }) + + it("Should not redeem an invite if the given identifier does not belong to an admin", async () => { + const invite = await invitesService.createInvite( + { groupId }, + admin.id + ) + + const fun = invitesService.redeemInviteKeyManually( + invite.code, + groupId, + "wrong-admin" + ) + + await expect(fun).rejects.toThrow("You are not an admin") + }) + }) + describe("# generateCode", () => { it("Should generate a random code with 8 characters", async () => { const result = (invitesService as any).generateCode() diff --git a/apps/api/src/app/invites/invites.service.ts b/apps/api/src/app/invites/invites.service.ts index 8f54ce86..589e080b 100644 --- a/apps/api/src/app/invites/invites.service.ts +++ b/apps/api/src/app/invites/invites.service.ts @@ -121,6 +121,7 @@ export class InvitesService { * Redeems an invite by consuming its code. Every invite * can be used only once. * @param inviteCode Invite code to be redeemed. + * @param groupId Group id. * @returns The updated invite. */ async redeemInvite(inviteCode: string, groupId: string): Promise { @@ -149,6 +150,42 @@ export class InvitesService { return this.inviteRepository.save(invite) } + /** + * Redeems an invite by using API Key. + * @param inviteCode Invite code to be redeemed. + * @param groupId Group id. + * @param apiKey The API Key. + * @returns The updated invite. + */ + async redeemInviteWithApiKey( + inviteCode: string, + groupId: string, + apiKey: string + ) { + await getAndCheckAdmin(this.adminsService, apiKey, groupId) + + return this.redeemInvite(inviteCode, groupId) + } + + /** + * Redeems an invite manually without using API Key. + * @param inviteCode Invite code to be redeemed. + * @param groupId Group id. + * @param adminId Group admin id. + * @returns The updated invite. + */ + async redeemInviteKeyManually( + inviteCode: string, + groupId: string, + adminId: string + ) { + const admin = await this.adminsService.findOne({ id: adminId }) + + if (!admin) throw new BadRequestException(`You are not an admin`) + + return this.redeemInvite(inviteCode, groupId) + } + /** * Generates a random code with a given number of characters. * The list of available characters have been chosen to be human readable. From 8149fd529186d23f6b1ca71e17279b0e9bc093a2 Mon Sep 17 00:00:00 2001 From: Han <56923450+waddaboo@users.noreply.github.com> Date: Fri, 1 Nov 2024 20:47:30 +0800 Subject: [PATCH 5/9] feat(api): add security to redeem invite endpoint --- .../api/src/app/invites/invites.controller.ts | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/api/src/app/invites/invites.controller.ts b/apps/api/src/app/invites/invites.controller.ts index d689acb2..016d564a 100644 --- a/apps/api/src/app/invites/invites.controller.ts +++ b/apps/api/src/app/invites/invites.controller.ts @@ -74,16 +74,34 @@ export class InvitesController { } @Post("redeem/:code/group/:group") + @ApiHeader({ name: "x-api-key", required: true }) @ApiOperation({ description: "Redeems a specific invite." }) @ApiCreatedResponse({ type: InviteResponse }) async redeemInvite( + @Headers() headers: Headers, + @Req() req: Request, @Param("code") inviteCode: string, @Param("group") groupId: string ): Promise { - const invite = await this.invitesService.redeemInvite( - inviteCode, - groupId - ) + let invite: Invite + + const apiKey = headers["x-api-key"] as string + + if (apiKey) { + invite = await this.invitesService.redeemInviteWithApiKey( + inviteCode, + groupId, + apiKey + ) + } else if (req.session.adminId) { + invite = await this.invitesService.redeemInviteKeyManually( + inviteCode, + groupId, + req.session.adminId + ) + } else { + throw new NotImplementedException() + } return mapEntity(invite) } From d3a05fadf7c89c3247a36886db77d250a13575e8 Mon Sep 17 00:00:00 2001 From: Han <56923450+waddaboo@users.noreply.github.com> Date: Fri, 1 Nov 2024 20:52:59 +0800 Subject: [PATCH 6/9] feat(api-sdk): add redeem invite api key --- libs/api-sdk/src/apiSdk.ts | 14 ++++++++++++-- libs/api-sdk/src/index.test.ts | 7 ++++++- libs/api-sdk/src/invites.ts | 5 ++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/libs/api-sdk/src/apiSdk.ts b/libs/api-sdk/src/apiSdk.ts index d26323ef..f0f6b672 100644 --- a/libs/api-sdk/src/apiSdk.ts +++ b/libs/api-sdk/src/apiSdk.ts @@ -423,10 +423,20 @@ export default class ApiSdk { * Redeems a specific invite. * @param inviteCode Invite code. * @param groupId Group id. + * @param apiKey The api key. * @returns The updated invite. */ - async redeemInvite(inviteCode: string, groupId: string): Promise { - const invite = await redeemInvite(this._config, inviteCode, groupId) + async redeemInvite( + inviteCode: string, + groupId: string, + apiKey: string + ): Promise { + const invite = await redeemInvite( + this._config, + inviteCode, + groupId, + apiKey + ) return invite } diff --git a/libs/api-sdk/src/index.test.ts b/libs/api-sdk/src/index.test.ts index 35d27f0e..200fcb42 100644 --- a/libs/api-sdk/src/index.test.ts +++ b/libs/api-sdk/src/index.test.ts @@ -1171,6 +1171,7 @@ describe("Bandada API SDK", () => { createdAt: "2023-08-09T18:09:53.000Z", updatedAt: "2023-08-09T18:09:53.000Z" } + const apiKey = "70f07d0d-6aa2-4fe1-b4b9-06c271a641dc" const inviteCode = "C5VAG4HD" const inviteCreatedAt = "2023-08-09T18:10:02.000Z" @@ -1184,7 +1185,11 @@ describe("Bandada API SDK", () => { ) const apiSdk: ApiSdk = new ApiSdk(SupportedUrl.DEV) - const invite = await apiSdk.redeemInvite(inviteCode, groupId) + const invite = await apiSdk.redeemInvite( + inviteCode, + groupId, + apiKey + ) expect(invite.code).toBe(inviteCode) expect(invite.isRedeemed).toBe(true) diff --git a/libs/api-sdk/src/invites.ts b/libs/api-sdk/src/invites.ts index ba2d4b71..61532b3d 100644 --- a/libs/api-sdk/src/invites.ts +++ b/libs/api-sdk/src/invites.ts @@ -50,7 +50,8 @@ export async function createInvite( export async function redeemInvite( config: object, inviteCode: string, - groupId: string + groupId: string, + apiKey: string ): Promise { const requestUrl = `${url}/redeem/${inviteCode}/group/${groupId}` @@ -59,6 +60,8 @@ export async function redeemInvite( ...config } + newConfig.headers["x-api-key"] = apiKey + const req = await request(requestUrl, newConfig) return req From 720b76ef901fb8836b4ae3e8f326d7f38d03ecca Mon Sep 17 00:00:00 2001 From: Han <56923450+waddaboo@users.noreply.github.com> Date: Fri, 1 Nov 2024 20:58:01 +0800 Subject: [PATCH 7/9] feat(client): update redeem invite --- apps/client/src/api/bandadaAPI.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/client/src/api/bandadaAPI.ts b/apps/client/src/api/bandadaAPI.ts index 661d2391..6d629200 100644 --- a/apps/client/src/api/bandadaAPI.ts +++ b/apps/client/src/api/bandadaAPI.ts @@ -1,6 +1,8 @@ import { ApiSdk, Group, Invite } from "@bandada/api-sdk" +import { request } from "@bandada/utils" const api = new ApiSdk(import.meta.env.VITE_API_URL) +const API_URL = import.meta.env.VITE_API_URL export async function getInvite(inviteCode: string): Promise { try { @@ -78,7 +80,12 @@ export async function redeemInvite( groupId: string ): Promise { try { - return await api.redeemInvite(inviteCode, groupId) + return await request( + `${API_URL}/invites/redeem/${inviteCode}/group/${groupId}`, + { + method: "post" + } + ) } catch (error: any) { console.error(error) From ebbf4d66b31d65b4663578a5d0a99886e989e3be Mon Sep 17 00:00:00 2001 From: Han <56923450+waddaboo@users.noreply.github.com> Date: Sat, 2 Nov 2024 01:23:26 +0800 Subject: [PATCH 8/9] refactor(api): update redeemInvite method, move param to body --- apps/api/src/app/invites/dto/redeem-invite.dto.ts | 15 +++++++++++++++ apps/api/src/app/invites/invites.controller.ts | 8 +++++--- 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 apps/api/src/app/invites/dto/redeem-invite.dto.ts diff --git a/apps/api/src/app/invites/dto/redeem-invite.dto.ts b/apps/api/src/app/invites/dto/redeem-invite.dto.ts new file mode 100644 index 00000000..eebe6ba0 --- /dev/null +++ b/apps/api/src/app/invites/dto/redeem-invite.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from "@nestjs/swagger" +import { IsNumberString, IsString, Length } from "class-validator" + +export class RedeemInviteDto { + @IsString() + @Length(8) + @ApiProperty() + readonly inviteCode: string + + @IsString() + @Length(32) + @IsNumberString() + @ApiProperty() + readonly groupId: string +} diff --git a/apps/api/src/app/invites/invites.controller.ts b/apps/api/src/app/invites/invites.controller.ts index 016d564a..41b0ed05 100644 --- a/apps/api/src/app/invites/invites.controller.ts +++ b/apps/api/src/app/invites/invites.controller.ts @@ -5,6 +5,7 @@ import { Headers, NotImplementedException, Param, + Patch, Post, Req } from "@nestjs/common" @@ -22,6 +23,7 @@ import { CreateInviteDto } from "./dto/create-invite.dto" import { Invite } from "./entities/invite.entity" import { InvitesService } from "./invites.service" import { mapEntity } from "../utils" +import { RedeemInviteDto } from "./dto/redeem-invite.dto" @ApiTags("invites") @Controller("invites") @@ -73,15 +75,15 @@ export class InvitesController { return mapEntity(invite) } - @Post("redeem/:code/group/:group") + @Patch("redeem") + @ApiBody({ type: RedeemInviteDto }) @ApiHeader({ name: "x-api-key", required: true }) @ApiOperation({ description: "Redeems a specific invite." }) @ApiCreatedResponse({ type: InviteResponse }) async redeemInvite( @Headers() headers: Headers, @Req() req: Request, - @Param("code") inviteCode: string, - @Param("group") groupId: string + @Body() { inviteCode, groupId }: RedeemInviteDto ): Promise { let invite: Invite From 6cd256fe643dac4581375ade71e7c8bf62aa2570 Mon Sep 17 00:00:00 2001 From: Han <56923450+waddaboo@users.noreply.github.com> Date: Sat, 2 Nov 2024 01:24:20 +0800 Subject: [PATCH 9/9] refactor(api-sdk): update redeemInvite method and body --- libs/api-sdk/src/invites.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/api-sdk/src/invites.ts b/libs/api-sdk/src/invites.ts index 61532b3d..70d1031d 100644 --- a/libs/api-sdk/src/invites.ts +++ b/libs/api-sdk/src/invites.ts @@ -53,10 +53,14 @@ export async function redeemInvite( groupId: string, apiKey: string ): Promise { - const requestUrl = `${url}/redeem/${inviteCode}/group/${groupId}` + const requestUrl = `${url}/redeem` const newConfig: any = { - method: "post", + method: "patch", + data: { + inviteCode, + groupId + }, ...config }