mirror of
https://github.com/AtHeartEngineering/bandada.git
synced 2026-01-09 03:38: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 { Global, Module } from "@nestjs/common"
|
||||||
import { TypeOrmModule } from "@nestjs/typeorm"
|
import { TypeOrmModule } from "@nestjs/typeorm"
|
||||||
import { Admin } from "./entities/admin.entity"
|
import { Admin } from "./entities/admin.entity"
|
||||||
import { AdminService } from "./admins.service"
|
import { AdminsService } from "./admins.service"
|
||||||
|
import { AdminsController } from "./admins.controller"
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Admin])],
|
imports: [TypeOrmModule.forFeature([Admin])],
|
||||||
exports: [AdminService],
|
exports: [AdminsService],
|
||||||
providers: [AdminService],
|
providers: [AdminsService],
|
||||||
controllers: []
|
controllers: [AdminsController]
|
||||||
})
|
})
|
||||||
export class AdminsModule {}
|
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 */
|
/* istanbul ignore file */
|
||||||
import { id } from "@ethersproject/hash"
|
import { id } from "@ethersproject/hash"
|
||||||
import { Injectable } from "@nestjs/common"
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
Logger
|
||||||
|
} from "@nestjs/common"
|
||||||
import { InjectRepository } from "@nestjs/typeorm"
|
import { InjectRepository } from "@nestjs/typeorm"
|
||||||
import { FindOptionsWhere, Repository } from "typeorm"
|
import { FindOptionsWhere, Repository } from "typeorm"
|
||||||
|
import { v4 } from "uuid"
|
||||||
import { CreateAdminDTO } from "./dto/create-admin.dto"
|
import { CreateAdminDTO } from "./dto/create-admin.dto"
|
||||||
import { Admin } from "./entities/admin.entity"
|
import { Admin } from "./entities/admin.entity"
|
||||||
|
import { UpdateApiKeyDTO } from "./dto/update-apikey.dto"
|
||||||
|
import { ApiKeyActions } from "../../types"
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminsService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Admin)
|
@InjectRepository(Admin)
|
||||||
private readonly adminRepository: Repository<Admin>
|
private readonly adminRepository: Repository<Admin>
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
public async create(payload: CreateAdminDTO): Promise<Admin> {
|
public async create(payload: CreateAdminDTO): Promise<Admin> {
|
||||||
const username = payload.username || payload.address.slice(-5)
|
const username = payload.username || payload.address.slice(-5)
|
||||||
@@ -29,4 +36,50 @@ export class AdminService {
|
|||||||
): Promise<Admin> {
|
): Promise<Admin> {
|
||||||
return this.adminRepository.findOneBy(payload)
|
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")
|
@Entity("admins")
|
||||||
export class Admin {
|
export class Admin {
|
||||||
@@ -12,6 +18,15 @@ export class Admin {
|
|||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
username: string
|
username: string
|
||||||
|
|
||||||
|
@Column({ name: "api_key", nullable: true })
|
||||||
|
apiKey: string
|
||||||
|
|
||||||
|
@Column({ name: "api_enabled", default: false })
|
||||||
|
apiEnabled: boolean
|
||||||
|
|
||||||
@CreateDateColumn({ name: "created_at" })
|
@CreateDateColumn({ name: "created_at" })
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: "updated_at" })
|
||||||
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
UnauthorizedException
|
UnauthorizedException
|
||||||
} from "@nestjs/common"
|
} from "@nestjs/common"
|
||||||
import { AdminService } from "../admins/admins.service"
|
import { AdminsService } from "../admins/admins.service"
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthGuard implements CanActivate {
|
export class AuthGuard implements CanActivate {
|
||||||
constructor(private adminService: AdminService) {}
|
constructor(private adminsService: AdminsService) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const req = context.switchToHttp().getRequest()
|
const req = context.switchToHttp().getRequest()
|
||||||
@@ -21,7 +21,7 @@ export class AuthGuard implements CanActivate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const admin = await this.adminService.findOne({ id: adminId })
|
const admin = await this.adminsService.findOne({ id: adminId })
|
||||||
|
|
||||||
req["admin"] = admin
|
req["admin"] = admin
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { TypeOrmModule } from "@nestjs/typeorm"
|
|||||||
import { ethers } from "ethers"
|
import { ethers } from "ethers"
|
||||||
import { generateNonce, SiweMessage } from "siwe"
|
import { generateNonce, SiweMessage } from "siwe"
|
||||||
import { Admin } from "../admins/entities/admin.entity"
|
import { Admin } from "../admins/entities/admin.entity"
|
||||||
import { AdminService } from "../admins/admins.service"
|
import { AdminsService } from "../admins/admins.service"
|
||||||
import { AuthService } from "./auth.service"
|
import { AuthService } from "./auth.service"
|
||||||
|
|
||||||
jest.mock("@bandada/utils", () => ({
|
jest.mock("@bandada/utils", () => ({
|
||||||
@@ -47,7 +47,7 @@ function createSiweMessage(address: string, statement?: string) {
|
|||||||
|
|
||||||
describe("AuthService", () => {
|
describe("AuthService", () => {
|
||||||
let authService: AuthService
|
let authService: AuthService
|
||||||
let adminService: AdminService
|
let adminsService: AdminsService
|
||||||
|
|
||||||
let originalApiUrl: string
|
let originalApiUrl: string
|
||||||
|
|
||||||
@@ -65,11 +65,11 @@ describe("AuthService", () => {
|
|||||||
}),
|
}),
|
||||||
TypeOrmModule.forFeature([Admin])
|
TypeOrmModule.forFeature([Admin])
|
||||||
],
|
],
|
||||||
providers: [AuthService, AdminService]
|
providers: [AuthService, AdminsService]
|
||||||
}).compile()
|
}).compile()
|
||||||
|
|
||||||
authService = await module.resolve(AuthService)
|
authService = await module.resolve(AuthService)
|
||||||
adminService = await module.resolve(AdminService)
|
adminsService = await module.resolve(AdminsService)
|
||||||
|
|
||||||
// Set API_URL so auth service can validate domain
|
// Set API_URL so auth service can validate domain
|
||||||
originalApiUrl = process.env.DASHBOARD_URL
|
originalApiUrl = process.env.DASHBOARD_URL
|
||||||
@@ -169,7 +169,7 @@ describe("AuthService", () => {
|
|||||||
|
|
||||||
describe("# isLoggedIn", () => {
|
describe("# isLoggedIn", () => {
|
||||||
it("Should return true if the admin exists", async () => {
|
it("Should return true if the admin exists", async () => {
|
||||||
const admin = await adminService.findOne({
|
const admin = await adminsService.findOne({
|
||||||
address: account1.address
|
address: account1.address
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import {
|
|||||||
} from "@nestjs/common"
|
} from "@nestjs/common"
|
||||||
import { SiweMessage } from "siwe"
|
import { SiweMessage } from "siwe"
|
||||||
import { v4 } from "uuid"
|
import { v4 } from "uuid"
|
||||||
import { AdminService } from "../admins/admins.service"
|
import { AdminsService } from "../admins/admins.service"
|
||||||
import { SignInWithEthereumDTO } from "./dto/siwe.dto"
|
import { SignInWithEthereumDTO } from "./dto/siwe.dto"
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(private readonly adminService: AdminService) {}
|
constructor(private readonly adminsService: AdminsService) {}
|
||||||
|
|
||||||
async signIn(
|
async signIn(
|
||||||
{ message, signature }: SignInWithEthereumDTO,
|
{ 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) {
|
if (!admin) {
|
||||||
admin = await this.adminService.create({
|
admin = await this.adminsService.create({
|
||||||
id: v4(),
|
id: v4(),
|
||||||
address
|
address
|
||||||
})
|
})
|
||||||
@@ -50,6 +50,6 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isLoggedIn(adminId: string): Promise<boolean> {
|
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 { OAuthAccount } from "./entities/credentials-account.entity"
|
||||||
import { CredentialsController } from "./credentials.controller"
|
import { CredentialsController } from "./credentials.controller"
|
||||||
import { CredentialsService } from "./credentials.service"
|
import { CredentialsService } from "./credentials.service"
|
||||||
|
import { AdminsModule } from "../admins/admins.module"
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
forwardRef(() => GroupsModule),
|
forwardRef(() => GroupsModule),
|
||||||
TypeOrmModule.forFeature([OAuthAccount])
|
TypeOrmModule.forFeature([OAuthAccount]),
|
||||||
|
AdminsModule
|
||||||
],
|
],
|
||||||
controllers: [CredentialsController],
|
controllers: [CredentialsController],
|
||||||
providers: [CredentialsService],
|
providers: [CredentialsService],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Invite } from "../invites/entities/invite.entity"
|
|||||||
import { InvitesService } from "../invites/invites.service"
|
import { InvitesService } from "../invites/invites.service"
|
||||||
import { OAuthAccount } from "./entities/credentials-account.entity"
|
import { OAuthAccount } from "./entities/credentials-account.entity"
|
||||||
import { CredentialsService } from "./credentials.service"
|
import { CredentialsService } from "./credentials.service"
|
||||||
|
import { AdminsModule } from "../admins/admins.module"
|
||||||
|
|
||||||
jest.mock("@bandada/utils", () => ({
|
jest.mock("@bandada/utils", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
@@ -59,7 +60,8 @@ describe("CredentialsService", () => {
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forFeature([Group, Invite, Member, OAuthAccount]),
|
TypeOrmModule.forFeature([Group, Invite, Member, OAuthAccount]),
|
||||||
ScheduleModule.forRoot()
|
ScheduleModule.forRoot(),
|
||||||
|
AdminsModule
|
||||||
],
|
],
|
||||||
providers: [GroupsService, InvitesService, CredentialsService]
|
providers: [GroupsService, InvitesService, CredentialsService]
|
||||||
}).compile()
|
}).compile()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
IsBoolean,
|
|
||||||
IsJSON,
|
IsJSON,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
@@ -21,10 +20,6 @@ export class UpdateGroupDto {
|
|||||||
@Max(32)
|
@Max(32)
|
||||||
readonly treeDepth?: number
|
readonly treeDepth?: number
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
readonly apiEnabled?: boolean
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
readonly fingerprintDuration?: number
|
readonly fingerprintDuration?: number
|
||||||
|
|||||||
@@ -59,12 +59,6 @@ export class Group {
|
|||||||
})
|
})
|
||||||
credentials: any // TODO: Add correct type for credentials JSON
|
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" })
|
@CreateDateColumn({ name: "created_at" })
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
ApiQuery,
|
ApiQuery,
|
||||||
ApiTags
|
ApiTags
|
||||||
} from "@nestjs/swagger"
|
} from "@nestjs/swagger"
|
||||||
import { ThrottlerGuard } from "@nestjs/throttler"
|
|
||||||
import { Request } from "express"
|
import { Request } from "express"
|
||||||
import { AuthGuard } from "../auth/auth.guard"
|
import { AuthGuard } from "../auth/auth.guard"
|
||||||
import { stringifyJSON } from "../utils"
|
import { stringifyJSON } from "../utils"
|
||||||
@@ -56,14 +55,15 @@ export class GroupsController {
|
|||||||
@Get(":group")
|
@Get(":group")
|
||||||
@ApiOperation({ description: "Returns a specific group." })
|
@ApiOperation({ description: "Returns a specific group." })
|
||||||
@ApiCreatedResponse({ type: 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 group = await this.groupsService.getGroup(groupId)
|
||||||
const fingerprint = await this.groupsService.getFingerprint(groupId)
|
const fingerprint = await this.groupsService.getFingerprint(groupId)
|
||||||
|
|
||||||
return mapGroupToResponseDTO(
|
return mapGroupToResponseDTO(
|
||||||
group,
|
group,
|
||||||
fingerprint,
|
fingerprint
|
||||||
req.session.adminId === group.adminId
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +79,7 @@ export class GroupsController {
|
|||||||
|
|
||||||
return mapGroupToResponseDTO(
|
return mapGroupToResponseDTO(
|
||||||
group,
|
group,
|
||||||
fingerprint,
|
fingerprint
|
||||||
req.session.adminId === group.adminId
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,19 +108,10 @@ export class GroupsController {
|
|||||||
|
|
||||||
return mapGroupToResponseDTO(
|
return mapGroupToResponseDTO(
|
||||||
group,
|
group,
|
||||||
fingerprint,
|
fingerprint
|
||||||
req.session.adminId === group.adminId
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@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")
|
@Get(":group/members/:member")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -6,15 +6,19 @@ import { Group } from "./entities/group.entity"
|
|||||||
import { Member } from "./entities/member.entity"
|
import { Member } from "./entities/member.entity"
|
||||||
import { GroupsController } from "./groups.controller"
|
import { GroupsController } from "./groups.controller"
|
||||||
import { GroupsService } from "./groups.service"
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
forwardRef(() => InvitesModule),
|
forwardRef(() => InvitesModule),
|
||||||
TypeOrmModule.forFeature([Member, Group])
|
TypeOrmModule.forFeature([Member, Group, Admin]),
|
||||||
|
AdminsModule
|
||||||
],
|
],
|
||||||
controllers: [GroupsController],
|
controllers: [GroupsController],
|
||||||
providers: [GroupsService],
|
providers: [GroupsService, AdminsService],
|
||||||
exports: [GroupsService]
|
exports: [GroupsService]
|
||||||
})
|
})
|
||||||
export class GroupsModule {}
|
export class GroupsModule {}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import { OAuthAccount } from "../credentials/entities/credentials-account.entity
|
|||||||
import { Group } from "./entities/group.entity"
|
import { Group } from "./entities/group.entity"
|
||||||
import { Member } from "./entities/member.entity"
|
import { Member } from "./entities/member.entity"
|
||||||
import { GroupsService } from "./groups.service"
|
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", () => ({
|
jest.mock("@bandada/utils", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
@@ -23,6 +27,7 @@ jest.mock("@bandada/utils", () => ({
|
|||||||
describe("GroupsService", () => {
|
describe("GroupsService", () => {
|
||||||
let groupsService: GroupsService
|
let groupsService: GroupsService
|
||||||
let invitesService: InvitesService
|
let invitesService: InvitesService
|
||||||
|
let adminsService: AdminsService
|
||||||
let groupId: string
|
let groupId: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -33,18 +38,20 @@ describe("GroupsService", () => {
|
|||||||
type: "sqlite",
|
type: "sqlite",
|
||||||
database: ":memory:",
|
database: ":memory:",
|
||||||
dropSchema: true,
|
dropSchema: true,
|
||||||
entities: [Group, Invite, Member, OAuthAccount],
|
entities: [Group, Invite, Member, OAuthAccount, Admin],
|
||||||
synchronize: true
|
synchronize: true
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forFeature([Group, Invite, Member]),
|
TypeOrmModule.forFeature([Group, Invite, Member, Admin]),
|
||||||
ScheduleModule.forRoot()
|
ScheduleModule.forRoot(),
|
||||||
|
AdminsModule
|
||||||
],
|
],
|
||||||
providers: [GroupsService, InvitesService]
|
providers: [GroupsService, InvitesService, AdminsService]
|
||||||
}).compile()
|
}).compile()
|
||||||
|
|
||||||
groupsService = await module.resolve(GroupsService)
|
groupsService = await module.resolve(GroupsService)
|
||||||
invitesService = await module.resolve(InvitesService)
|
invitesService = await module.resolve(InvitesService)
|
||||||
|
adminsService = await module.resolve(AdminsService)
|
||||||
|
|
||||||
await groupsService.initialize()
|
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", () => {
|
describe("# addMember", () => {
|
||||||
let invite: Invite
|
let invite: Invite
|
||||||
|
|
||||||
@@ -424,10 +383,21 @@ describe("GroupsService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("# Add and remove member via API", () => {
|
describe("# Add and remove member via API", () => {
|
||||||
|
let admin: Admin
|
||||||
let group: Group
|
let group: Group
|
||||||
let apiKey: string
|
let apiKey: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
admin = await adminsService.create({
|
||||||
|
id: "admin",
|
||||||
|
address: "0x"
|
||||||
|
})
|
||||||
|
|
||||||
|
apiKey = await adminsService.updateApiKey({
|
||||||
|
adminId: admin.id,
|
||||||
|
action: ApiKeyActions.Generate
|
||||||
|
})
|
||||||
|
|
||||||
group = await groupsService.createGroup(
|
group = await groupsService.createGroup(
|
||||||
{
|
{
|
||||||
name: "Group2",
|
name: "Group2",
|
||||||
@@ -435,16 +405,10 @@ describe("GroupsService", () => {
|
|||||||
treeDepth: 16,
|
treeDepth: 16,
|
||||||
fingerprintDuration: 3600
|
fingerprintDuration: 3600
|
||||||
},
|
},
|
||||||
"admin"
|
admin.id
|
||||||
)
|
)
|
||||||
|
|
||||||
await groupsService.updateGroup(
|
admin = await adminsService.findOne({ id: admin.id })
|
||||||
group.id,
|
|
||||||
{ apiEnabled: true },
|
|
||||||
"admin"
|
|
||||||
)
|
|
||||||
|
|
||||||
apiKey = (await groupsService.getGroup(group.id)).apiKey
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should add a member to an existing group via API", async () => {
|
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 () => {
|
it("Should not add a member to an existing group if API belongs to another admin", async () => {
|
||||||
await groupsService.updateGroup(
|
|
||||||
group.id,
|
|
||||||
{ apiEnabled: false },
|
|
||||||
"admin"
|
|
||||||
)
|
|
||||||
|
|
||||||
const fun = groupsService.addMemberWithAPIKey(
|
const fun = groupsService.addMemberWithAPIKey(
|
||||||
groupId,
|
groupId,
|
||||||
"100002",
|
"100002",
|
||||||
apiKey
|
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(
|
await expect(fun).rejects.toThrow(
|
||||||
"Invalid API key or API access not enabled for group"
|
"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 () => {
|
it("Should not remove a member to an existing group if API is disabled", async () => {
|
||||||
const fun = groupsService.removeMemberWithAPIKey(
|
const fun = groupsService.removeMemberWithAPIKey(
|
||||||
groupId,
|
group.id,
|
||||||
"100001",
|
"100001",
|
||||||
apiKey
|
apiKey
|
||||||
)
|
)
|
||||||
@@ -525,10 +536,21 @@ describe("GroupsService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("# Add and remove members via API", () => {
|
describe("# Add and remove members via API", () => {
|
||||||
|
let admin: Admin
|
||||||
let group: Group
|
let group: Group
|
||||||
let apiKey: string
|
let apiKey: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
admin = await adminsService.create({
|
||||||
|
id: "admin",
|
||||||
|
address: "0x"
|
||||||
|
})
|
||||||
|
|
||||||
|
apiKey = await adminsService.updateApiKey({
|
||||||
|
adminId: admin.id,
|
||||||
|
action: ApiKeyActions.Generate
|
||||||
|
})
|
||||||
|
|
||||||
group = await groupsService.createGroup(
|
group = await groupsService.createGroup(
|
||||||
{
|
{
|
||||||
name: "Group2",
|
name: "Group2",
|
||||||
@@ -536,16 +558,10 @@ describe("GroupsService", () => {
|
|||||||
treeDepth: 16,
|
treeDepth: 16,
|
||||||
fingerprintDuration: 3600
|
fingerprintDuration: 3600
|
||||||
},
|
},
|
||||||
"admin"
|
admin.id
|
||||||
)
|
)
|
||||||
|
|
||||||
await groupsService.updateGroup(
|
admin = await adminsService.findOne({ id: admin.id })
|
||||||
group.id,
|
|
||||||
{ apiEnabled: true },
|
|
||||||
"admin"
|
|
||||||
)
|
|
||||||
|
|
||||||
apiKey = (await groupsService.getGroup(group.id)).apiKey
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should add a member to an existing group via API", async () => {
|
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 () => {
|
it("Should not add a member to an existing group if API belongs to another admin", async () => {
|
||||||
await groupsService.updateGroup(
|
|
||||||
group.id,
|
|
||||||
{ apiEnabled: false },
|
|
||||||
"admin"
|
|
||||||
)
|
|
||||||
|
|
||||||
const fun = groupsService.addMembersWithAPIKey(
|
const fun = groupsService.addMembersWithAPIKey(
|
||||||
groupId,
|
groupId,
|
||||||
["100002"],
|
["100002"],
|
||||||
apiKey
|
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(
|
await expect(fun).rejects.toThrow(
|
||||||
"Invalid API key or API access not enabled for group"
|
"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 () => {
|
it("Should not remove a member to an existing group if API is disabled", async () => {
|
||||||
const fun = groupsService.removeMembersWithAPIKey(
|
const fun = groupsService.removeMembersWithAPIKey(
|
||||||
groupId,
|
group.id,
|
||||||
["100001"],
|
["100001"],
|
||||||
apiKey
|
apiKey
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
import { InjectRepository } from "@nestjs/typeorm"
|
import { InjectRepository } from "@nestjs/typeorm"
|
||||||
import { Group as CachedGroup } from "@semaphore-protocol/group"
|
import { Group as CachedGroup } from "@semaphore-protocol/group"
|
||||||
import { Repository } from "typeorm"
|
import { Repository } from "typeorm"
|
||||||
import { v4 } from "uuid"
|
|
||||||
import { InvitesService } from "../invites/invites.service"
|
import { InvitesService } from "../invites/invites.service"
|
||||||
|
import { AdminsService } from "../admins/admins.service"
|
||||||
import { CreateGroupDto } from "./dto/create-group.dto"
|
import { CreateGroupDto } from "./dto/create-group.dto"
|
||||||
import { UpdateGroupDto } from "./dto/update-group.dto"
|
import { UpdateGroupDto } from "./dto/update-group.dto"
|
||||||
import { Group } from "./entities/group.entity"
|
import { Group } from "./entities/group.entity"
|
||||||
@@ -31,7 +31,8 @@ export class GroupsService {
|
|||||||
@InjectRepository(Member)
|
@InjectRepository(Member)
|
||||||
private readonly memberRepository: Repository<Member>,
|
private readonly memberRepository: Repository<Member>,
|
||||||
@Inject(forwardRef(() => InvitesService))
|
@Inject(forwardRef(() => InvitesService))
|
||||||
private readonly invitesService: InvitesService
|
private readonly invitesService: InvitesService,
|
||||||
|
private readonly adminsService: AdminsService
|
||||||
) {
|
) {
|
||||||
this.cachedGroups = new Map()
|
this.cachedGroups = new Map()
|
||||||
// this.bandadaContract = getBandadaContract(
|
// this.bandadaContract = getBandadaContract(
|
||||||
@@ -138,7 +139,6 @@ export class GroupsService {
|
|||||||
{
|
{
|
||||||
description,
|
description,
|
||||||
treeDepth,
|
treeDepth,
|
||||||
apiEnabled,
|
|
||||||
credentials,
|
credentials,
|
||||||
fingerprintDuration
|
fingerprintDuration
|
||||||
}: UpdateGroupDto,
|
}: UpdateGroupDto,
|
||||||
@@ -176,15 +176,6 @@ export class GroupsService {
|
|||||||
group.credentials = credentials
|
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)
|
await this.groupRepository.save(group)
|
||||||
|
|
||||||
Logger.log(`GroupsService: group '${group.name}' has been updated`)
|
Logger.log(`GroupsService: group '${group.name}' has been updated`)
|
||||||
@@ -192,37 +183,6 @@ export class GroupsService {
|
|||||||
return group
|
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.
|
* Join the group by redeeming invite code.
|
||||||
* @param groupId Group id.
|
* @param groupId Group id.
|
||||||
@@ -320,8 +280,15 @@ export class GroupsService {
|
|||||||
apiKey: string
|
apiKey: string
|
||||||
): Promise<Group> {
|
): Promise<Group> {
|
||||||
const group = await this.getGroup(groupId)
|
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(
|
throw new BadRequestException(
|
||||||
`Invalid API key or API access not enabled for group '${groupId}'`
|
`Invalid API key or API access not enabled for group '${groupId}'`
|
||||||
)
|
)
|
||||||
@@ -540,8 +507,15 @@ export class GroupsService {
|
|||||||
apiKey: string
|
apiKey: string
|
||||||
): Promise<Group> {
|
): Promise<Group> {
|
||||||
const group = await this.getGroup(groupId)
|
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(
|
throw new BadRequestException(
|
||||||
`Invalid API key or API access not enabled for group '${groupId}'`
|
`Invalid API key or API access not enabled for group '${groupId}'`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,17 +23,6 @@ describe("Groups utils", () => {
|
|||||||
expect(members).toHaveLength(0)
|
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 () => {
|
it("Should map the fingerprint correctly", async () => {
|
||||||
const { fingerprint } = mapGroupToResponseDTO({} as any, "12345")
|
const { fingerprint } = mapGroupToResponseDTO({} as any, "12345")
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { Group } from "./entities/group.entity"
|
import { Group } from "./entities/group.entity"
|
||||||
|
|
||||||
export function mapGroupToResponseDTO(
|
export function mapGroupToResponseDTO(group: Group, fingerprint: string = "") {
|
||||||
group: Group,
|
|
||||||
fingerprint: string = "",
|
|
||||||
includeAPIKey: boolean = false
|
|
||||||
) {
|
|
||||||
const dto = {
|
const dto = {
|
||||||
id: group.id,
|
id: group.id,
|
||||||
name: group.name,
|
name: group.name,
|
||||||
@@ -15,14 +11,7 @@ export function mapGroupToResponseDTO(
|
|||||||
fingerprintDuration: group.fingerprintDuration,
|
fingerprintDuration: group.fingerprintDuration,
|
||||||
createdAt: group.createdAt,
|
createdAt: group.createdAt,
|
||||||
members: (group.members || []).map((m) => m.id),
|
members: (group.members || []).map((m) => m.id),
|
||||||
credentials: group.credentials,
|
credentials: group.credentials
|
||||||
apiKey: undefined,
|
|
||||||
apiEnabled: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeAPIKey) {
|
|
||||||
dto.apiKey = group.apiKey
|
|
||||||
dto.apiEnabled = group.apiEnabled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dto
|
return dto
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { GroupsModule } from "../groups/groups.module"
|
|||||||
import { Invite } from "./entities/invite.entity"
|
import { Invite } from "./entities/invite.entity"
|
||||||
import { InvitesController } from "./invites.controller"
|
import { InvitesController } from "./invites.controller"
|
||||||
import { InvitesService } from "./invites.service"
|
import { InvitesService } from "./invites.service"
|
||||||
|
import { AdminsModule } from "../admins/admins.module"
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
forwardRef(() => GroupsModule),
|
forwardRef(() => GroupsModule),
|
||||||
TypeOrmModule.forFeature([Invite])
|
TypeOrmModule.forFeature([Invite]),
|
||||||
|
AdminsModule
|
||||||
],
|
],
|
||||||
controllers: [InvitesController],
|
controllers: [InvitesController],
|
||||||
providers: [InvitesService],
|
providers: [InvitesService],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { GroupsService } from "../groups/groups.service"
|
|||||||
import { OAuthAccount } from "../credentials/entities/credentials-account.entity"
|
import { OAuthAccount } from "../credentials/entities/credentials-account.entity"
|
||||||
import { Invite } from "./entities/invite.entity"
|
import { Invite } from "./entities/invite.entity"
|
||||||
import { InvitesService } from "./invites.service"
|
import { InvitesService } from "./invites.service"
|
||||||
|
import { AdminsModule } from "../admins/admins.module"
|
||||||
|
|
||||||
jest.mock("@bandada/utils", () => ({
|
jest.mock("@bandada/utils", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
@@ -37,7 +38,8 @@ describe("InvitesService", () => {
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forFeature([Group, Invite, Member]),
|
TypeOrmModule.forFeature([Group, Invite, Member]),
|
||||||
ScheduleModule.forRoot()
|
ScheduleModule.forRoot(),
|
||||||
|
AdminsModule
|
||||||
],
|
],
|
||||||
providers: [GroupsService, InvitesService]
|
providers: [GroupsService, InvitesService]
|
||||||
}).compile()
|
}).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,
|
id character varying PRIMARY KEY,
|
||||||
address character varying NOT NULL UNIQUE,
|
address character varying NOT NULL UNIQUE,
|
||||||
username 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 ----------------------------------------------
|
-- Table Definition ----------------------------------------------
|
||||||
@@ -17,8 +20,6 @@ CREATE TABLE groups (
|
|||||||
tree_depth integer NOT NULL,
|
tree_depth integer NOT NULL,
|
||||||
fingerprint_duration integer NOT NULL,
|
fingerprint_duration integer NOT NULL,
|
||||||
credentials text,
|
credentials text,
|
||||||
api_enabled boolean NOT NULL DEFAULT false,
|
|
||||||
api_key character varying,
|
|
||||||
created_at timestamp without time zone NOT NULL DEFAULT now(),
|
created_at timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
updated_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