Skip to content

Commit

Permalink
Merge pull request #475 from bandada-infra/api/createInvite
Browse files Browse the repository at this point in the history
Add missing `createInvite` api endpoint
  • Loading branch information
vplasencia authored Apr 8, 2024
2 parents 367f7c6 + 841e44c commit d9f7a26
Show file tree
Hide file tree
Showing 20 changed files with 569 additions and 167 deletions.
4 changes: 0 additions & 4 deletions apps/api/src/app/groups/docSchemas/groupResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ export class GroupResponse {
@ApiProperty()
credentials: object
@ApiProperty()
apiEnabled: boolean
@ApiProperty()
apiKey: string
@ApiProperty()
createdAt: Date
@ApiProperty()
updatedAt: Date
Expand Down
4 changes: 0 additions & 4 deletions apps/api/src/app/groups/docSchemas/inviteResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,4 @@ export class InviteResponse {
createdAt: Date
@ApiProperty()
group: GroupResponse
@ApiProperty()
groupName: string
@ApiProperty()
groupId: string
}
2 changes: 1 addition & 1 deletion apps/api/src/app/groups/groups.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
69 changes: 1 addition & 68 deletions apps/api/src/app/groups/groups.utils.test.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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({
Expand All @@ -37,7 +35,6 @@ describe("Groups utils", () => {
}).compile()

groupsService = await module.resolve(GroupsService)
adminsService = await module.resolve(AdminsService)

await groupsService.initialize()
})
Expand Down Expand Up @@ -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}'`
)
})
})
})
27 changes: 0 additions & 27 deletions apps/api/src/app/groups/groups.utils.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -19,27 +16,3 @@ export function mapGroupToResponseDTO(group: Group, fingerprint: string = "") {

return dto
}

export async function getAndCheckAdmin(
adminService: AdminsService,
apiKey: string,
groupId?: string
): Promise<Admin> {
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
}
2 changes: 2 additions & 0 deletions apps/api/src/app/invites/dto/create-invite.dto.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ApiProperty } from "@nestjs/swagger"
import { IsNumberString, IsString, Length } from "class-validator"

export class CreateInviteDto {
@IsString()
@Length(32)
@IsNumberString()
@ApiProperty()
readonly groupId: string
}
2 changes: 1 addition & 1 deletion apps/api/src/app/invites/entities/invite.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class Invite {
code: string

@Column({ default: false, name: "is_redeemed" })
isRedeemed?: boolean
isRedeemed: boolean

@ManyToOne(() => Group, {
onDelete: "CASCADE"
Expand Down
56 changes: 36 additions & 20 deletions apps/api/src/app/invites/invites.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,73 @@ import {
Body,
Controller,
Get,
Headers,
NotImplementedException,
Param,
Post,
Req,
UseGuards
Req
} from "@nestjs/common"
import {
ApiBody,
ApiCreatedResponse,
ApiExcludeEndpoint,
ApiHeader,
ApiOperation,
ApiTags
} from "@nestjs/swagger"
import { ThrottlerGuard } from "@nestjs/throttler"
// import { ThrottlerGuard } from "@nestjs/throttler"
import { Request } from "express"
import { AuthGuard } from "../auth/auth.guard"
import { mapEntity } from "../utils"
import { InviteResponse } from "../groups/docSchemas"
import { CreateInviteDto } from "./dto/create-invite.dto"
import { Invite } from "./entities/invite.entity"
import { InvitesService } from "./invites.service"
import { mapEntity } from "../utils"

@ApiTags("invites")
@Controller("invites")
export class InvitesController {
constructor(private readonly invitesService: InvitesService) {}

@Post()
@UseGuards(AuthGuard)
@UseGuards(ThrottlerGuard)
@ApiExcludeEndpoint()
// @UseGuards(ThrottlerGuard)
@ApiBody({ type: CreateInviteDto })
@ApiHeader({ name: "x-api-key", required: true })
@ApiCreatedResponse({ type: InviteResponse })
@ApiOperation({
description: "Creates a new group invite with a unique code."
})
async createInvite(
@Headers() headers: Headers,
@Req() req: Request,
@Body() dto: CreateInviteDto
): Promise<string> {
const { code } = await this.invitesService.createInvite(
dto,
req.session.adminId
)
): Promise<InviteResponse> {
let invite: Invite

const apiKey = headers["x-api-key"] as string

return code
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 mapEntity(invite)
}

@Get(":code")
@ApiOperation({ description: "Returns a specific invite." })
@ApiCreatedResponse({ type: InviteResponse })
async getInvite(
@Param("code") inviteCode: string
): Promise<Omit<Invite, "id">> {
const invite = (await this.invitesService.getInvite(inviteCode)) as any

invite.groupName = invite.group.name
invite.groupId = invite.group.id
): Promise<InviteResponse> {
const invite = await this.invitesService.getInvite(inviteCode)

return mapEntity(invite)
}
Expand Down
6 changes: 4 additions & 2 deletions apps/api/src/app/invites/invites.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Loading

0 comments on commit d9f7a26

Please sign in to comment.