refactor: move apikey logic from group to admin

This commit is contained in:
Jeeiii
2024-03-21 23:58:13 +01:00
parent 496bbbb532
commit 784929c897
23 changed files with 510 additions and 210 deletions

View 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
})
}
}

View File

@@ -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 {}

View 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`
)
})
})
})

View File

@@ -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
}
}

View 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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
})

View File

@@ -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 }))
}
}

View File

@@ -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],

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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 {}

View File

@@ -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
)

View File

@@ -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}'`
)

View File

@@ -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")

View File

@@ -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

View File

@@ -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],

View File

@@ -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()

View 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"
}

View File

@@ -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()
);