mirror of
https://github.com/AtHeartEngineering/bandada.git
synced 2026-01-08 04:54:16 -05:00
refactor: move apikey logic from group to admin
This commit is contained in:
23
apps/api/src/app/admins/admins.controller.ts
Normal file
23
apps/api/src/app/admins/admins.controller.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Body, Controller, Post, Put } from "@nestjs/common"
|
||||
import { CreateAdminDTO } from "./dto/create-admin.dto"
|
||||
import { UpdateApiKeyDTO } from "./dto/update-apikey.dto"
|
||||
import { AdminsService } from "./admins.service"
|
||||
import { Admin } from "./entities/admin.entity"
|
||||
|
||||
@Controller("admins")
|
||||
export class AdminsController {
|
||||
constructor(private readonly adminsService: AdminsService) {}
|
||||
|
||||
@Post()
|
||||
async createAdmin(@Body() dto: CreateAdminDTO): Promise<Admin> {
|
||||
return this.adminsService.create(dto)
|
||||
}
|
||||
|
||||
@Put("update-apikey")
|
||||
async updateApiKey(@Body() dto: UpdateApiKeyDTO): Promise<string> {
|
||||
return this.adminsService.updateApiKey({
|
||||
adminId: dto.adminId,
|
||||
action: dto.action
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Global, Module } from "@nestjs/common"
|
||||
import { TypeOrmModule } from "@nestjs/typeorm"
|
||||
import { Admin } from "./entities/admin.entity"
|
||||
import { AdminService } from "./admins.service"
|
||||
import { AdminsService } from "./admins.service"
|
||||
import { AdminsController } from "./admins.controller"
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Admin])],
|
||||
exports: [AdminService],
|
||||
providers: [AdminService],
|
||||
controllers: []
|
||||
exports: [AdminsService],
|
||||
providers: [AdminsService],
|
||||
controllers: [AdminsController]
|
||||
})
|
||||
export class AdminsModule {}
|
||||
|
||||
181
apps/api/src/app/admins/admins.service.test.ts
Normal file
181
apps/api/src/app/admins/admins.service.test.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { id as idToHash } from "@ethersproject/hash"
|
||||
import { ScheduleModule } from "@nestjs/schedule"
|
||||
import { Test } from "@nestjs/testing"
|
||||
import { TypeOrmModule } from "@nestjs/typeorm"
|
||||
import { AdminsService } from "./admins.service"
|
||||
import { Admin } from "./entities/admin.entity"
|
||||
import { ApiKeyActions } from "../../types"
|
||||
|
||||
describe("AdminsService", () => {
|
||||
const id = "1"
|
||||
const hashedId = idToHash(id)
|
||||
const address = "0x000000"
|
||||
let admin: Admin
|
||||
let adminsService: AdminsService
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [
|
||||
TypeOrmModule.forRootAsync({
|
||||
useFactory: () => ({
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
dropSchema: true,
|
||||
entities: [Admin],
|
||||
synchronize: true
|
||||
})
|
||||
}),
|
||||
TypeOrmModule.forFeature([Admin]),
|
||||
ScheduleModule.forRoot()
|
||||
],
|
||||
providers: [AdminsService]
|
||||
}).compile()
|
||||
adminsService = await module.resolve(AdminsService)
|
||||
})
|
||||
|
||||
describe("# create", () => {
|
||||
it("Should create an admin", async () => {
|
||||
admin = await adminsService.create({ id, address })
|
||||
|
||||
expect(admin.id).toBe(idToHash(id))
|
||||
expect(admin.address).toBe(address)
|
||||
expect(admin.username).toBe(address.slice(-5))
|
||||
expect(admin.apiEnabled).toBeFalsy()
|
||||
expect(admin.apiKey).toBeNull()
|
||||
})
|
||||
|
||||
it("Should create an admin given the username", async () => {
|
||||
const id2 = "2"
|
||||
const address2 = "0x000002"
|
||||
const username = "admn2"
|
||||
|
||||
const admin = await adminsService.create({
|
||||
id: id2,
|
||||
address: address2,
|
||||
username
|
||||
})
|
||||
|
||||
expect(admin.id).toBe(idToHash(id2))
|
||||
expect(admin.address).toBe(address2)
|
||||
expect(admin.username).toBe(username)
|
||||
expect(admin.apiEnabled).toBeFalsy()
|
||||
expect(admin.apiKey).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("# findOne", () => {
|
||||
it("Should return the admin given the identifier", async () => {
|
||||
const found = await adminsService.findOne({ id: hashedId })
|
||||
|
||||
expect(found.id).toBe(admin.id)
|
||||
expect(found.address).toBe(admin.address)
|
||||
expect(found.username).toBe(admin.username)
|
||||
expect(found.apiEnabled).toBeFalsy()
|
||||
expect(found.apiKey).toBe(admin.apiKey)
|
||||
})
|
||||
|
||||
it("Should return null if the given identifier does not belong to an admin", async () => {
|
||||
expect(await adminsService.findOne({ id: "3" })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("# updateApiKey", () => {
|
||||
it("Should create an apikey for the admin", async () => {
|
||||
const apiKey = await adminsService.updateApiKey({
|
||||
adminId: hashedId,
|
||||
action: ApiKeyActions.Generate
|
||||
})
|
||||
|
||||
admin = await adminsService.findOne({ id: hashedId })
|
||||
|
||||
expect(admin.apiEnabled).toBeTruthy()
|
||||
expect(admin.apiKey).toBe(apiKey)
|
||||
})
|
||||
|
||||
it("Should generate another apikey for the admin", async () => {
|
||||
const previousApiKey = admin.apiKey
|
||||
|
||||
const apiKey = await adminsService.updateApiKey({
|
||||
adminId: hashedId,
|
||||
action: ApiKeyActions.Generate
|
||||
})
|
||||
|
||||
admin = await adminsService.findOne({ id: hashedId })
|
||||
|
||||
expect(admin.apiEnabled).toBeTruthy()
|
||||
expect(admin.apiKey).toBe(apiKey)
|
||||
expect(admin.apiKey).not.toBe(previousApiKey)
|
||||
})
|
||||
|
||||
it("Should disable the apikey for the admin", async () => {
|
||||
const { apiKey } = admin
|
||||
|
||||
await adminsService.updateApiKey({
|
||||
adminId: hashedId,
|
||||
action: ApiKeyActions.Disable
|
||||
})
|
||||
|
||||
admin = await adminsService.findOne({ id: hashedId })
|
||||
|
||||
expect(admin.apiEnabled).toBeFalsy()
|
||||
expect(admin.apiKey).toBe(apiKey)
|
||||
})
|
||||
|
||||
it("Should enable the apikey for the admin", async () => {
|
||||
const { apiKey } = admin
|
||||
|
||||
await adminsService.updateApiKey({
|
||||
adminId: hashedId,
|
||||
action: ApiKeyActions.Enable
|
||||
})
|
||||
|
||||
admin = await adminsService.findOne({ id: hashedId })
|
||||
|
||||
expect(admin.apiEnabled).toBeTruthy()
|
||||
expect(admin.apiKey).toBe(apiKey)
|
||||
})
|
||||
|
||||
it("Should not create the apikey when the given id does not belog to an admin", async () => {
|
||||
const wrongId = "wrongId"
|
||||
|
||||
const fun = adminsService.updateApiKey({
|
||||
adminId: wrongId,
|
||||
action: ApiKeyActions.Disable
|
||||
})
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
`The '${wrongId}' does not belong to an admin`
|
||||
)
|
||||
})
|
||||
|
||||
it("Should not enable the apikey before creation", async () => {
|
||||
const tempAdmin = await adminsService.create({
|
||||
id: "id2",
|
||||
address: "address2"
|
||||
})
|
||||
|
||||
const fun = adminsService.updateApiKey({
|
||||
adminId: tempAdmin.id,
|
||||
action: ApiKeyActions.Enable
|
||||
})
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
`The '${tempAdmin.id}' does not have an apikey`
|
||||
)
|
||||
})
|
||||
|
||||
it("Shoul throw if the action does not exist", async () => {
|
||||
const wrongAction = "wrong-action"
|
||||
|
||||
const fun = adminsService.updateApiKey({
|
||||
adminId: hashedId,
|
||||
// @ts-ignore
|
||||
action: wrongAction
|
||||
})
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
`Unsupported ${wrongAction} apikey`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,17 +1,24 @@
|
||||
/* istanbul ignore file */
|
||||
import { id } from "@ethersproject/hash"
|
||||
import { Injectable } from "@nestjs/common"
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger
|
||||
} from "@nestjs/common"
|
||||
import { InjectRepository } from "@nestjs/typeorm"
|
||||
import { FindOptionsWhere, Repository } from "typeorm"
|
||||
import { v4 } from "uuid"
|
||||
import { CreateAdminDTO } from "./dto/create-admin.dto"
|
||||
import { Admin } from "./entities/admin.entity"
|
||||
import { UpdateApiKeyDTO } from "./dto/update-apikey.dto"
|
||||
import { ApiKeyActions } from "../../types"
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
export class AdminsService {
|
||||
constructor(
|
||||
@InjectRepository(Admin)
|
||||
private readonly adminRepository: Repository<Admin>
|
||||
) {}
|
||||
) { }
|
||||
|
||||
public async create(payload: CreateAdminDTO): Promise<Admin> {
|
||||
const username = payload.username || payload.address.slice(-5)
|
||||
@@ -29,4 +36,50 @@ export class AdminService {
|
||||
): Promise<Admin> {
|
||||
return this.adminRepository.findOneBy(payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the API key for a given admin based on the specified actions.
|
||||
*
|
||||
* @param {UpdateApiKeyDTO} updateApiKeyDTO The DTO containing the admin ID and the action to be performed.
|
||||
* @returns {Promise<string>} The API key of the admin after the update operation. If the API key is disabled, the return value might not be meaningful.
|
||||
* @throws {BadRequestException} If the admin ID does not correspond to an existing admin, if the admin does not have an API key when trying to enable it, or if the action is unsupported.
|
||||
*/
|
||||
async updateApiKey({ adminId, action }: UpdateApiKeyDTO): Promise<string> {
|
||||
const admin = await this.findOne({
|
||||
id: adminId
|
||||
})
|
||||
|
||||
if (!admin) {
|
||||
throw new BadRequestException(
|
||||
`The '${adminId}' does not belong to an admin`
|
||||
)
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case ApiKeyActions.Generate:
|
||||
admin.apiKey = v4()
|
||||
admin.apiEnabled = true
|
||||
break
|
||||
case ApiKeyActions.Enable:
|
||||
if (!admin.apiKey)
|
||||
throw new BadRequestException(
|
||||
`The '${adminId}' does not have an apikey`
|
||||
)
|
||||
admin.apiEnabled = true
|
||||
break
|
||||
case ApiKeyActions.Disable:
|
||||
admin.apiEnabled = false
|
||||
break
|
||||
default:
|
||||
throw new BadRequestException(`Unsupported ${action} apikey`)
|
||||
}
|
||||
|
||||
await this.adminRepository.save(admin)
|
||||
|
||||
Logger.log(
|
||||
`AdminsService: admin '${admin.id}' api key have been updated`
|
||||
)
|
||||
|
||||
return admin.apiKey
|
||||
}
|
||||
}
|
||||
|
||||
10
apps/api/src/app/admins/dto/update-apikey.dto.ts
Normal file
10
apps/api/src/app/admins/dto/update-apikey.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { IsEnum, IsString } from "class-validator"
|
||||
import { ApiKeyActions } from "../../../types"
|
||||
|
||||
export class UpdateApiKeyDTO {
|
||||
@IsString()
|
||||
adminId: string
|
||||
|
||||
@IsEnum(ApiKeyActions)
|
||||
action: ApiKeyActions
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Column, CreateDateColumn, Entity, PrimaryColumn } from "typeorm"
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn
|
||||
} from "typeorm"
|
||||
|
||||
@Entity("admins")
|
||||
export class Admin {
|
||||
@@ -12,6 +18,15 @@ export class Admin {
|
||||
@Column({ unique: true })
|
||||
username: string
|
||||
|
||||
@Column({ name: "api_key", nullable: true })
|
||||
apiKey: string
|
||||
|
||||
@Column({ name: "api_enabled", default: false })
|
||||
apiEnabled: boolean
|
||||
|
||||
@CreateDateColumn({ name: "created_at" })
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn({ name: "updated_at" })
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
Injectable,
|
||||
UnauthorizedException
|
||||
} from "@nestjs/common"
|
||||
import { AdminService } from "../admins/admins.service"
|
||||
import { AdminsService } from "../admins/admins.service"
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private adminService: AdminService) {}
|
||||
constructor(private adminsService: AdminsService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest()
|
||||
@@ -21,7 +21,7 @@ export class AuthGuard implements CanActivate {
|
||||
}
|
||||
|
||||
try {
|
||||
const admin = await this.adminService.findOne({ id: adminId })
|
||||
const admin = await this.adminsService.findOne({ id: adminId })
|
||||
|
||||
req["admin"] = admin
|
||||
} catch {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TypeOrmModule } from "@nestjs/typeorm"
|
||||
import { ethers } from "ethers"
|
||||
import { generateNonce, SiweMessage } from "siwe"
|
||||
import { Admin } from "../admins/entities/admin.entity"
|
||||
import { AdminService } from "../admins/admins.service"
|
||||
import { AdminsService } from "../admins/admins.service"
|
||||
import { AuthService } from "./auth.service"
|
||||
|
||||
jest.mock("@bandada/utils", () => ({
|
||||
@@ -47,7 +47,7 @@ function createSiweMessage(address: string, statement?: string) {
|
||||
|
||||
describe("AuthService", () => {
|
||||
let authService: AuthService
|
||||
let adminService: AdminService
|
||||
let adminsService: AdminsService
|
||||
|
||||
let originalApiUrl: string
|
||||
|
||||
@@ -65,11 +65,11 @@ describe("AuthService", () => {
|
||||
}),
|
||||
TypeOrmModule.forFeature([Admin])
|
||||
],
|
||||
providers: [AuthService, AdminService]
|
||||
providers: [AuthService, AdminsService]
|
||||
}).compile()
|
||||
|
||||
authService = await module.resolve(AuthService)
|
||||
adminService = await module.resolve(AdminService)
|
||||
adminsService = await module.resolve(AdminsService)
|
||||
|
||||
// Set API_URL so auth service can validate domain
|
||||
originalApiUrl = process.env.DASHBOARD_URL
|
||||
@@ -169,7 +169,7 @@ describe("AuthService", () => {
|
||||
|
||||
describe("# isLoggedIn", () => {
|
||||
it("Should return true if the admin exists", async () => {
|
||||
const admin = await adminService.findOne({
|
||||
const admin = await adminsService.findOne({
|
||||
address: account1.address
|
||||
})
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
} from "@nestjs/common"
|
||||
import { SiweMessage } from "siwe"
|
||||
import { v4 } from "uuid"
|
||||
import { AdminService } from "../admins/admins.service"
|
||||
import { AdminsService } from "../admins/admins.service"
|
||||
import { SignInWithEthereumDTO } from "./dto/siwe.dto"
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(private readonly adminService: AdminService) {}
|
||||
constructor(private readonly adminsService: AdminsService) {}
|
||||
|
||||
async signIn(
|
||||
{ message, signature }: SignInWithEthereumDTO,
|
||||
@@ -37,10 +37,10 @@ export class AuthService {
|
||||
)
|
||||
}
|
||||
|
||||
let admin = await this.adminService.findOne({ address })
|
||||
let admin = await this.adminsService.findOne({ address })
|
||||
|
||||
if (!admin) {
|
||||
admin = await this.adminService.create({
|
||||
admin = await this.adminsService.create({
|
||||
id: v4(),
|
||||
address
|
||||
})
|
||||
@@ -50,6 +50,6 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async isLoggedIn(adminId: string): Promise<boolean> {
|
||||
return !!(await this.adminService.findOne({ id: adminId }))
|
||||
return !!(await this.adminsService.findOne({ id: adminId }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ import { GroupsModule } from "../groups/groups.module"
|
||||
import { OAuthAccount } from "./entities/credentials-account.entity"
|
||||
import { CredentialsController } from "./credentials.controller"
|
||||
import { CredentialsService } from "./credentials.service"
|
||||
import { AdminsModule } from "../admins/admins.module"
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ScheduleModule.forRoot(),
|
||||
forwardRef(() => GroupsModule),
|
||||
TypeOrmModule.forFeature([OAuthAccount])
|
||||
TypeOrmModule.forFeature([OAuthAccount]),
|
||||
AdminsModule
|
||||
],
|
||||
controllers: [CredentialsController],
|
||||
providers: [CredentialsService],
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Invite } from "../invites/entities/invite.entity"
|
||||
import { InvitesService } from "../invites/invites.service"
|
||||
import { OAuthAccount } from "./entities/credentials-account.entity"
|
||||
import { CredentialsService } from "./credentials.service"
|
||||
import { AdminsModule } from "../admins/admins.module"
|
||||
|
||||
jest.mock("@bandada/utils", () => ({
|
||||
__esModule: true,
|
||||
@@ -59,7 +60,8 @@ describe("CredentialsService", () => {
|
||||
})
|
||||
}),
|
||||
TypeOrmModule.forFeature([Group, Invite, Member, OAuthAccount]),
|
||||
ScheduleModule.forRoot()
|
||||
ScheduleModule.forRoot(),
|
||||
AdminsModule
|
||||
],
|
||||
providers: [GroupsService, InvitesService, CredentialsService]
|
||||
}).compile()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
IsBoolean,
|
||||
IsJSON,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
@@ -21,10 +20,6 @@ export class UpdateGroupDto {
|
||||
@Max(32)
|
||||
readonly treeDepth?: number
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly apiEnabled?: boolean
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
readonly fingerprintDuration?: number
|
||||
|
||||
@@ -59,12 +59,6 @@ export class Group {
|
||||
})
|
||||
credentials: any // TODO: Add correct type for credentials JSON
|
||||
|
||||
@Column({ name: "api_enabled", default: false })
|
||||
apiEnabled: boolean
|
||||
|
||||
@Column({ name: "api_key", nullable: true })
|
||||
apiKey: string
|
||||
|
||||
@CreateDateColumn({ name: "created_at" })
|
||||
createdAt: Date
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
ApiQuery,
|
||||
ApiTags
|
||||
} from "@nestjs/swagger"
|
||||
import { ThrottlerGuard } from "@nestjs/throttler"
|
||||
import { Request } from "express"
|
||||
import { AuthGuard } from "../auth/auth.guard"
|
||||
import { stringifyJSON } from "../utils"
|
||||
@@ -56,14 +55,15 @@ export class GroupsController {
|
||||
@Get(":group")
|
||||
@ApiOperation({ description: "Returns a specific group." })
|
||||
@ApiCreatedResponse({ type: Group })
|
||||
async getGroup(@Param("group") groupId: string, @Req() req: Request) {
|
||||
async getGroup(
|
||||
@Param("group") groupId: string
|
||||
) {
|
||||
const group = await this.groupsService.getGroup(groupId)
|
||||
const fingerprint = await this.groupsService.getFingerprint(groupId)
|
||||
|
||||
return mapGroupToResponseDTO(
|
||||
group,
|
||||
fingerprint,
|
||||
req.session.adminId === group.adminId
|
||||
fingerprint
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,8 +79,7 @@ export class GroupsController {
|
||||
|
||||
return mapGroupToResponseDTO(
|
||||
group,
|
||||
fingerprint,
|
||||
req.session.adminId === group.adminId
|
||||
fingerprint
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,19 +108,10 @@ export class GroupsController {
|
||||
|
||||
return mapGroupToResponseDTO(
|
||||
group,
|
||||
fingerprint,
|
||||
req.session.adminId === group.adminId
|
||||
fingerprint
|
||||
)
|
||||
}
|
||||
|
||||
@Patch(":group/api-key")
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(ThrottlerGuard)
|
||||
@ApiExcludeEndpoint()
|
||||
async updateApiKey(@Req() req: Request, @Param("group") groupId: string) {
|
||||
return this.groupsService.updateApiKey(groupId, req.session.adminId)
|
||||
}
|
||||
|
||||
@Get(":group/members/:member")
|
||||
@ApiOperation({
|
||||
description:
|
||||
|
||||
@@ -6,15 +6,19 @@ import { Group } from "./entities/group.entity"
|
||||
import { Member } from "./entities/member.entity"
|
||||
import { GroupsController } from "./groups.controller"
|
||||
import { GroupsService } from "./groups.service"
|
||||
import { AdminsModule } from "../admins/admins.module"
|
||||
import { Admin } from "../admins/entities/admin.entity"
|
||||
import { AdminsService } from "../admins/admins.service"
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ScheduleModule.forRoot(),
|
||||
forwardRef(() => InvitesModule),
|
||||
TypeOrmModule.forFeature([Member, Group])
|
||||
TypeOrmModule.forFeature([Member, Group, Admin]),
|
||||
AdminsModule
|
||||
],
|
||||
controllers: [GroupsController],
|
||||
providers: [GroupsService],
|
||||
providers: [GroupsService, AdminsService],
|
||||
exports: [GroupsService]
|
||||
})
|
||||
export class GroupsModule {}
|
||||
|
||||
@@ -7,6 +7,10 @@ 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 { ApiKeyActions } from "../../types"
|
||||
|
||||
jest.mock("@bandada/utils", () => ({
|
||||
__esModule: true,
|
||||
@@ -23,6 +27,7 @@ jest.mock("@bandada/utils", () => ({
|
||||
describe("GroupsService", () => {
|
||||
let groupsService: GroupsService
|
||||
let invitesService: InvitesService
|
||||
let adminsService: AdminsService
|
||||
let groupId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -33,18 +38,20 @@ describe("GroupsService", () => {
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
dropSchema: true,
|
||||
entities: [Group, Invite, Member, OAuthAccount],
|
||||
entities: [Group, Invite, Member, OAuthAccount, Admin],
|
||||
synchronize: true
|
||||
})
|
||||
}),
|
||||
TypeOrmModule.forFeature([Group, Invite, Member]),
|
||||
ScheduleModule.forRoot()
|
||||
TypeOrmModule.forFeature([Group, Invite, Member, Admin]),
|
||||
ScheduleModule.forRoot(),
|
||||
AdminsModule
|
||||
],
|
||||
providers: [GroupsService, InvitesService]
|
||||
providers: [GroupsService, InvitesService, AdminsService]
|
||||
}).compile()
|
||||
|
||||
groupsService = await module.resolve(GroupsService)
|
||||
invitesService = await module.resolve(InvitesService)
|
||||
adminsService = await module.resolve(AdminsService)
|
||||
|
||||
await groupsService.initialize()
|
||||
|
||||
@@ -225,54 +232,6 @@ describe("GroupsService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("# updateApiKey", () => {
|
||||
let group: Group
|
||||
|
||||
it("Should enable the API with a new API key", async () => {
|
||||
group = await groupsService.createGroup(
|
||||
{
|
||||
name: "Group2",
|
||||
description: "This is a new group",
|
||||
treeDepth: 16,
|
||||
fingerprintDuration: 3600
|
||||
},
|
||||
"admin"
|
||||
)
|
||||
|
||||
await groupsService.updateGroup(
|
||||
group.id,
|
||||
{ apiEnabled: true },
|
||||
"admin"
|
||||
)
|
||||
|
||||
const { apiKey } = await groupsService.getGroup(group.id)
|
||||
|
||||
expect(apiKey).toHaveLength(36)
|
||||
})
|
||||
|
||||
it("Should update the api key of the group", async () => {
|
||||
const apiKey = await groupsService.updateApiKey(group.id, "admin")
|
||||
|
||||
expect(apiKey).toHaveLength(36)
|
||||
})
|
||||
|
||||
it("Should not update the api key if the admin is the wrong one", async () => {
|
||||
const fun = groupsService.updateApiKey(groupId, "wrong-admin")
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
`You are not the admin of the group '${groupId}'`
|
||||
)
|
||||
})
|
||||
|
||||
it("Should not update the api key if the api is not enabled", async () => {
|
||||
const fun = groupsService.updateApiKey(groupId, "admin")
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
`Group '${groupId}' API key is not enabled`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("# addMember", () => {
|
||||
let invite: Invite
|
||||
|
||||
@@ -424,10 +383,21 @@ describe("GroupsService", () => {
|
||||
})
|
||||
|
||||
describe("# Add and remove member via API", () => {
|
||||
let admin: Admin
|
||||
let group: Group
|
||||
let apiKey: string
|
||||
|
||||
beforeAll(async () => {
|
||||
admin = await adminsService.create({
|
||||
id: "admin",
|
||||
address: "0x"
|
||||
})
|
||||
|
||||
apiKey = await adminsService.updateApiKey({
|
||||
adminId: admin.id,
|
||||
action: ApiKeyActions.Generate
|
||||
})
|
||||
|
||||
group = await groupsService.createGroup(
|
||||
{
|
||||
name: "Group2",
|
||||
@@ -435,16 +405,10 @@ describe("GroupsService", () => {
|
||||
treeDepth: 16,
|
||||
fingerprintDuration: 3600
|
||||
},
|
||||
"admin"
|
||||
admin.id
|
||||
)
|
||||
|
||||
await groupsService.updateGroup(
|
||||
group.id,
|
||||
{ apiEnabled: true },
|
||||
"admin"
|
||||
)
|
||||
|
||||
apiKey = (await groupsService.getGroup(group.id)).apiKey
|
||||
admin = await adminsService.findOne({ id: admin.id })
|
||||
})
|
||||
|
||||
it("Should add a member to an existing group via API", async () => {
|
||||
@@ -493,19 +457,66 @@ describe("GroupsService", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("Should not add a member to an existing group if API is disabled", async () => {
|
||||
await groupsService.updateGroup(
|
||||
group.id,
|
||||
{ apiEnabled: false },
|
||||
"admin"
|
||||
)
|
||||
|
||||
it("Should not add a member to an existing group if API belongs to another admin", async () => {
|
||||
const fun = groupsService.addMemberWithAPIKey(
|
||||
groupId,
|
||||
"100002",
|
||||
apiKey
|
||||
)
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
`Invalid admin for group '${groupId}'`
|
||||
)
|
||||
})
|
||||
|
||||
it("Should not remove a member to an existing group if API belongs to another admin", async () => {
|
||||
const fun = groupsService.removeMemberWithAPIKey(
|
||||
groupId,
|
||||
"100001",
|
||||
apiKey
|
||||
)
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
`Invalid admin for group '${groupId}'`
|
||||
)
|
||||
})
|
||||
|
||||
it("Should not add a member to an existing group if API is invalid", async () => {
|
||||
const fun = groupsService.addMemberWithAPIKey(
|
||||
group.id,
|
||||
"100002",
|
||||
"apiKey"
|
||||
)
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
"Invalid API key or API access not enabled for group"
|
||||
)
|
||||
})
|
||||
|
||||
it("Should not remove a member to an existing group if API is invalid", async () => {
|
||||
const fun = groupsService.removeMemberWithAPIKey(
|
||||
group.id,
|
||||
"100001",
|
||||
"apiKey"
|
||||
)
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
"Invalid API key or API access not enabled for group"
|
||||
)
|
||||
})
|
||||
|
||||
it("Should not add a member to an existing group if API is disabled", async () => {
|
||||
await adminsService.updateApiKey({
|
||||
adminId: admin.id,
|
||||
action: ApiKeyActions.Disable
|
||||
})
|
||||
|
||||
const fun = groupsService.addMemberWithAPIKey(
|
||||
group.id,
|
||||
"100002",
|
||||
apiKey
|
||||
)
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
"Invalid API key or API access not enabled for group"
|
||||
)
|
||||
@@ -513,7 +524,7 @@ describe("GroupsService", () => {
|
||||
|
||||
it("Should not remove a member to an existing group if API is disabled", async () => {
|
||||
const fun = groupsService.removeMemberWithAPIKey(
|
||||
groupId,
|
||||
group.id,
|
||||
"100001",
|
||||
apiKey
|
||||
)
|
||||
@@ -525,10 +536,21 @@ describe("GroupsService", () => {
|
||||
})
|
||||
|
||||
describe("# Add and remove members via API", () => {
|
||||
let admin: Admin
|
||||
let group: Group
|
||||
let apiKey: string
|
||||
|
||||
beforeAll(async () => {
|
||||
admin = await adminsService.create({
|
||||
id: "admin",
|
||||
address: "0x"
|
||||
})
|
||||
|
||||
apiKey = await adminsService.updateApiKey({
|
||||
adminId: admin.id,
|
||||
action: ApiKeyActions.Generate
|
||||
})
|
||||
|
||||
group = await groupsService.createGroup(
|
||||
{
|
||||
name: "Group2",
|
||||
@@ -536,16 +558,10 @@ describe("GroupsService", () => {
|
||||
treeDepth: 16,
|
||||
fingerprintDuration: 3600
|
||||
},
|
||||
"admin"
|
||||
admin.id
|
||||
)
|
||||
|
||||
await groupsService.updateGroup(
|
||||
group.id,
|
||||
{ apiEnabled: true },
|
||||
"admin"
|
||||
)
|
||||
|
||||
apiKey = (await groupsService.getGroup(group.id)).apiKey
|
||||
admin = await adminsService.findOne({ id: admin.id })
|
||||
})
|
||||
|
||||
it("Should add a member to an existing group via API", async () => {
|
||||
@@ -600,19 +616,66 @@ describe("GroupsService", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("Should not add a member to an existing group if API is disabled", async () => {
|
||||
await groupsService.updateGroup(
|
||||
group.id,
|
||||
{ apiEnabled: false },
|
||||
"admin"
|
||||
)
|
||||
|
||||
it("Should not add a member to an existing group if API belongs to another admin", async () => {
|
||||
const fun = groupsService.addMembersWithAPIKey(
|
||||
groupId,
|
||||
["100002"],
|
||||
apiKey
|
||||
)
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
`Invalid admin for group '${groupId}'`
|
||||
)
|
||||
})
|
||||
|
||||
it("Should not remove a member to an existing group if API belongs to another admin", async () => {
|
||||
const fun = groupsService.removeMembersWithAPIKey(
|
||||
groupId,
|
||||
["100001"],
|
||||
apiKey
|
||||
)
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
`Invalid admin for group '${groupId}'`
|
||||
)
|
||||
})
|
||||
|
||||
it("Should not add a member to an existing group if API is invalid", async () => {
|
||||
const fun = groupsService.addMembersWithAPIKey(
|
||||
group.id,
|
||||
["100002"],
|
||||
"apiKey"
|
||||
)
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
"Invalid API key or API access not enabled for group"
|
||||
)
|
||||
})
|
||||
|
||||
it("Should not remove a member to an existing group if API is invalid", async () => {
|
||||
const fun = groupsService.removeMembersWithAPIKey(
|
||||
group.id,
|
||||
["100001"],
|
||||
"apiKey"
|
||||
)
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
"Invalid API key or API access not enabled for group"
|
||||
)
|
||||
})
|
||||
|
||||
it("Should not add a member to an existing group if API is disabled", async () => {
|
||||
await adminsService.updateApiKey({
|
||||
adminId: admin.id,
|
||||
action: ApiKeyActions.Disable
|
||||
})
|
||||
|
||||
const fun = groupsService.addMembersWithAPIKey(
|
||||
group.id,
|
||||
["100002"],
|
||||
apiKey
|
||||
)
|
||||
|
||||
await expect(fun).rejects.toThrow(
|
||||
"Invalid API key or API access not enabled for group"
|
||||
)
|
||||
@@ -620,7 +683,7 @@ describe("GroupsService", () => {
|
||||
|
||||
it("Should not remove a member to an existing group if API is disabled", async () => {
|
||||
const fun = groupsService.removeMembersWithAPIKey(
|
||||
groupId,
|
||||
group.id,
|
||||
["100001"],
|
||||
apiKey
|
||||
)
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
import { InjectRepository } from "@nestjs/typeorm"
|
||||
import { Group as CachedGroup } from "@semaphore-protocol/group"
|
||||
import { Repository } from "typeorm"
|
||||
import { v4 } from "uuid"
|
||||
import { InvitesService } from "../invites/invites.service"
|
||||
import { AdminsService } from "../admins/admins.service"
|
||||
import { CreateGroupDto } from "./dto/create-group.dto"
|
||||
import { UpdateGroupDto } from "./dto/update-group.dto"
|
||||
import { Group } from "./entities/group.entity"
|
||||
@@ -31,7 +31,8 @@ export class GroupsService {
|
||||
@InjectRepository(Member)
|
||||
private readonly memberRepository: Repository<Member>,
|
||||
@Inject(forwardRef(() => InvitesService))
|
||||
private readonly invitesService: InvitesService
|
||||
private readonly invitesService: InvitesService,
|
||||
private readonly adminsService: AdminsService
|
||||
) {
|
||||
this.cachedGroups = new Map()
|
||||
// this.bandadaContract = getBandadaContract(
|
||||
@@ -138,7 +139,6 @@ export class GroupsService {
|
||||
{
|
||||
description,
|
||||
treeDepth,
|
||||
apiEnabled,
|
||||
credentials,
|
||||
fingerprintDuration
|
||||
}: UpdateGroupDto,
|
||||
@@ -176,15 +176,6 @@ export class GroupsService {
|
||||
group.credentials = credentials
|
||||
}
|
||||
|
||||
if (!group.credentials && apiEnabled !== undefined) {
|
||||
group.apiEnabled = apiEnabled
|
||||
|
||||
// Generate a new API key if it doesn't exist
|
||||
if (!group.apiKey) {
|
||||
group.apiKey = v4()
|
||||
}
|
||||
}
|
||||
|
||||
await this.groupRepository.save(group)
|
||||
|
||||
Logger.log(`GroupsService: group '${group.name}' has been updated`)
|
||||
@@ -192,37 +183,6 @@ export class GroupsService {
|
||||
return group
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the group api key.
|
||||
* @param groupId Group id.
|
||||
* @param adminId Group admin id.
|
||||
*/
|
||||
async updateApiKey(groupId: string, adminId: string): Promise<string> {
|
||||
const group = await this.getGroup(groupId)
|
||||
|
||||
if (group.adminId !== adminId) {
|
||||
throw new UnauthorizedException(
|
||||
`You are not the admin of the group '${groupId}'`
|
||||
)
|
||||
}
|
||||
|
||||
if (!group.apiEnabled) {
|
||||
throw new UnauthorizedException(
|
||||
`Group '${groupId}' API key is not enabled`
|
||||
)
|
||||
}
|
||||
|
||||
group.apiKey = v4()
|
||||
|
||||
await this.groupRepository.save(group)
|
||||
|
||||
Logger.log(
|
||||
`GroupsService: group '${group.name}' APIs have been updated`
|
||||
)
|
||||
|
||||
return group.apiKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the group by redeeming invite code.
|
||||
* @param groupId Group id.
|
||||
@@ -320,8 +280,15 @@ export class GroupsService {
|
||||
apiKey: string
|
||||
): Promise<Group> {
|
||||
const group = await this.getGroup(groupId)
|
||||
const admin = await this.adminsService.findOne({ id: group.adminId })
|
||||
|
||||
if (!group.apiEnabled || group.apiKey !== apiKey) {
|
||||
if (!admin) {
|
||||
throw new BadRequestException(
|
||||
`Invalid admin for group '${groupId}'`
|
||||
)
|
||||
}
|
||||
|
||||
if (!admin.apiEnabled || admin.apiKey !== apiKey) {
|
||||
throw new BadRequestException(
|
||||
`Invalid API key or API access not enabled for group '${groupId}'`
|
||||
)
|
||||
@@ -540,8 +507,15 @@ export class GroupsService {
|
||||
apiKey: string
|
||||
): Promise<Group> {
|
||||
const group = await this.getGroup(groupId)
|
||||
const admin = await this.adminsService.findOne({ id: group.adminId })
|
||||
|
||||
if (!group.apiEnabled || group.apiKey !== apiKey) {
|
||||
if (!admin) {
|
||||
throw new BadRequestException(
|
||||
`Invalid admin for group '${groupId}'`
|
||||
)
|
||||
}
|
||||
|
||||
if (!admin.apiEnabled || admin.apiKey !== apiKey) {
|
||||
throw new BadRequestException(
|
||||
`Invalid API key or API access not enabled for group '${groupId}'`
|
||||
)
|
||||
|
||||
@@ -23,17 +23,6 @@ describe("Groups utils", () => {
|
||||
expect(members).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("Should map the group data with api keys if specified", async () => {
|
||||
const { apiKey, apiEnabled } = mapGroupToResponseDTO(
|
||||
{ apiEnabled: true, apiKey: "123" } as any,
|
||||
"12345",
|
||||
true
|
||||
)
|
||||
|
||||
expect(apiEnabled).toBeTruthy()
|
||||
expect(apiKey).toBe("123")
|
||||
})
|
||||
|
||||
it("Should map the fingerprint correctly", async () => {
|
||||
const { fingerprint } = mapGroupToResponseDTO({} as any, "12345")
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { Group } from "./entities/group.entity"
|
||||
|
||||
export function mapGroupToResponseDTO(
|
||||
group: Group,
|
||||
fingerprint: string = "",
|
||||
includeAPIKey: boolean = false
|
||||
) {
|
||||
export function mapGroupToResponseDTO(group: Group, fingerprint: string = "") {
|
||||
const dto = {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
@@ -15,14 +11,7 @@ export function mapGroupToResponseDTO(
|
||||
fingerprintDuration: group.fingerprintDuration,
|
||||
createdAt: group.createdAt,
|
||||
members: (group.members || []).map((m) => m.id),
|
||||
credentials: group.credentials,
|
||||
apiKey: undefined,
|
||||
apiEnabled: undefined
|
||||
}
|
||||
|
||||
if (includeAPIKey) {
|
||||
dto.apiKey = group.apiKey
|
||||
dto.apiEnabled = group.apiEnabled
|
||||
credentials: group.credentials
|
||||
}
|
||||
|
||||
return dto
|
||||
|
||||
@@ -4,11 +4,13 @@ import { GroupsModule } from "../groups/groups.module"
|
||||
import { Invite } from "./entities/invite.entity"
|
||||
import { InvitesController } from "./invites.controller"
|
||||
import { InvitesService } from "./invites.service"
|
||||
import { AdminsModule } from "../admins/admins.module"
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
forwardRef(() => GroupsModule),
|
||||
TypeOrmModule.forFeature([Invite])
|
||||
TypeOrmModule.forFeature([Invite]),
|
||||
AdminsModule
|
||||
],
|
||||
controllers: [InvitesController],
|
||||
providers: [InvitesService],
|
||||
|
||||
@@ -7,6 +7,7 @@ import { GroupsService } from "../groups/groups.service"
|
||||
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"
|
||||
|
||||
jest.mock("@bandada/utils", () => ({
|
||||
__esModule: true,
|
||||
@@ -37,7 +38,8 @@ describe("InvitesService", () => {
|
||||
})
|
||||
}),
|
||||
TypeOrmModule.forFeature([Group, Invite, Member]),
|
||||
ScheduleModule.forRoot()
|
||||
ScheduleModule.forRoot(),
|
||||
AdminsModule
|
||||
],
|
||||
providers: [GroupsService, InvitesService]
|
||||
}).compile()
|
||||
|
||||
10
apps/api/src/types/index.ts
Normal file
10
apps/api/src/types/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Defines the possible actions that can be performed on an API key.
|
||||
* This includes generating a new API key, enabling an existing API key,
|
||||
* and disabling an existing API key.
|
||||
*/
|
||||
export enum ApiKeyActions {
|
||||
Generate = "generate",
|
||||
Enable = "enable",
|
||||
Disable = "disable"
|
||||
}
|
||||
@@ -4,7 +4,10 @@ CREATE TABLE admins (
|
||||
id character varying PRIMARY KEY,
|
||||
address character varying NOT NULL UNIQUE,
|
||||
username character varying NOT NULL UNIQUE,
|
||||
created_at timestamp without time zone NOT NULL DEFAULT now()
|
||||
api_key character varying,
|
||||
api_enabled boolean NOT NULL DEFAULT false,
|
||||
created_at timestamp without time zone NOT NULL DEFAULT now(),
|
||||
updated_at timestamp without time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Table Definition ----------------------------------------------
|
||||
@@ -17,8 +20,6 @@ CREATE TABLE groups (
|
||||
tree_depth integer NOT NULL,
|
||||
fingerprint_duration integer NOT NULL,
|
||||
credentials text,
|
||||
api_enabled boolean NOT NULL DEFAULT false,
|
||||
api_key character varying,
|
||||
created_at timestamp without time zone NOT NULL DEFAULT now(),
|
||||
updated_at timestamp without time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user