diff --git a/apps/api-v2/src/app.module.ts b/apps/api-v2/src/app.module.ts index 422566f7..9c607c9d 100644 --- a/apps/api-v2/src/app.module.ts +++ b/apps/api-v2/src/app.module.ts @@ -5,6 +5,7 @@ import { PrismaService } from "./common/db/prisma.service"; import { AuthGuard } from "./common/guards/auth.guard"; import { ApplicationsModule } from "./sections/applications/applications.module"; import { AuthModule } from "./sections/auth/auth.module"; +import { MembersModule } from "./sections/members/members.module"; import { StatusModule } from "./sections/status/status.module"; import { UtilityModule } from "./sections/utility/utility.module"; @@ -14,6 +15,7 @@ import { UtilityModule } from "./sections/utility/utility.module"; AuthModule, ApplicationsModule, StatusModule, + MembersModule, ConfigModule.forRoot({ isGlobal: true, cache: true }), ], providers: [PrismaService, { provide: APP_GUARD, useClass: AuthGuard }], diff --git a/apps/api-v2/src/common/db/external/cachet.service.ts b/apps/api-v2/src/common/db/external/cachet.service.ts index 6fa87a93..3532e3cc 100644 --- a/apps/api-v2/src/common/db/external/cachet.service.ts +++ b/apps/api-v2/src/common/db/external/cachet.service.ts @@ -26,11 +26,10 @@ export class CachetAPIService { } else { this.baseURL = this.configService.get("CACHET_URL") as string; this.apiToken = this.configService.get("CACHET_TOKEN") as string; + this.testConnection().then(() => { + this.logger.log("Cachet API is reachable"); + }); } - - this.testConnection().then(() => { - this.logger.log("Cachet API is reachable"); - }); } async testConnection(): Promise { diff --git a/apps/api-v2/src/sections/members/dto/member.dto.ts b/apps/api-v2/src/sections/members/dto/member.dto.ts new file mode 100644 index 00000000..e1bc14e9 --- /dev/null +++ b/apps/api-v2/src/sections/members/dto/member.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from "@nestjs/swagger"; + +export class MemberDto { + @ApiProperty({ + example: "00000000-0000-0000-0000-000000000000", + description: "The unique ID of the user.", + }) + id: string; + + @ApiProperty({ + example: "000000000000000000", + description: "The discord ID of the user.", + }) + discordId: string; + + @ApiProperty({ + example: "myMinecraftName", + description: "The Minecraft username of the user.", + }) + minecraft: string; + + @ApiProperty({ + example: "myUsername", + description: "The username of the user.", + }) + username: string; +} diff --git a/apps/api-v2/src/sections/members/members.controller.ts b/apps/api-v2/src/sections/members/members.controller.ts new file mode 100644 index 00000000..c20f8d70 --- /dev/null +++ b/apps/api-v2/src/sections/members/members.controller.ts @@ -0,0 +1,68 @@ +import { Controller, Get, Req } from "@nestjs/common"; +import { ApiBearerAuth, ApiOperation } from "@nestjs/swagger"; +import { Request } from "express"; +import { + ApiErrorResponse, + ApiPaginatedResponseDto, +} from "src/common/decorators/api-response.decorator"; +import { Filter, FilterParams } from "src/common/decorators/filter.decorator"; +import { Filtered } from "src/common/decorators/filtered.decorator"; +import { Paginated } from "src/common/decorators/paginated.decorator"; +import { + Pagination, + PaginationParams, +} from "src/common/decorators/pagination.decorator"; +import { Sortable } from "src/common/decorators/sortable.decorator"; +import { + Sorting, + SortingParams, +} from "src/common/decorators/sorting.decorator"; +import { PaginatedControllerResponse } from "src/typings"; +import { MemberDto } from "./dto/member.dto"; +import { MembersService } from "./members.service"; + +@Controller("members") +export class MembersController { + constructor(private readonly membersService: MembersService) {} + + @Get("/") + @ApiBearerAuth() + @Sortable({ + defaultSortBy: "id", + allowedFields: ["id", "discordId", "minecraft", "username"], + defaultOrder: "desc", + }) + @Paginated() + @Filtered({ + fields: [ + { name: "id", required: false, type: String }, + { name: "discordId", required: false, type: String }, + { + name: "minecraft", + required: false, + type: String, + }, + { name: "username", required: false, type: String }, + ], + }) + @ApiOperation({ + summary: "Get All Members", + description: "Returns all members of the currently authenticated team.", + }) + @ApiPaginatedResponseDto(MemberDto, { description: "Success" }) + @ApiErrorResponse({ status: 401, description: "Unauthorized" }) + async getMembers( + @Pagination() pagination: PaginationParams, + @Sorting() sorting: SortingParams, + @Filter() filter: FilterParams, + @Req() req: Request, + ): PaginatedControllerResponse { + return await this.membersService.findAll( + req.token.id, + pagination, + sorting.sortBy, + sorting.order, + filter.filter, + ); + } +} diff --git a/apps/api-v2/src/sections/members/members.module.ts b/apps/api-v2/src/sections/members/members.module.ts new file mode 100644 index 00000000..f7a794e2 --- /dev/null +++ b/apps/api-v2/src/sections/members/members.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; +import { PrismaService } from "src/common/db/prisma.service"; +import { MembersController } from "./members.controller"; +import { MembersService } from "./members.service"; + +@Module({ + controllers: [MembersController], + providers: [MembersService, PrismaService], +}) +export class MembersModule {} diff --git a/apps/api-v2/src/sections/members/members.service.ts b/apps/api-v2/src/sections/members/members.service.ts new file mode 100644 index 00000000..c673a773 --- /dev/null +++ b/apps/api-v2/src/sections/members/members.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "src/common/db/prisma.service"; +import { FilterParams } from "src/common/decorators/filter.decorator"; +import { PaginationParams } from "src/common/decorators/pagination.decorator"; +import { SortingParams } from "src/common/decorators/sorting.decorator"; + +@Injectable() +export class MembersService { + constructor(private readonly prisma: PrismaService) {} + + async findAll( + buildteamId: string, + pagination: PaginationParams, + sortBy?: SortingParams["sortBy"], + order?: SortingParams["order"], + filter?: FilterParams["filter"], + ) { + const take = Math.max(Number(pagination.limit) || 20, 1); + const skip = Math.max((Number(pagination.page) || 1) - 1, 0) * take; + + const combinedFilter = { + ...filter, + ...{ joinedBuildTeams: { some: { id: buildteamId } } }, + }; + + const [users, count] = await Promise.all([ + this.prisma.user.findMany({ + where: combinedFilter, + orderBy: { [sortBy || "id"]: order === "desc" ? "desc" : "asc" }, + skip, + take, + select: { + id: true, + discordId: true, + minecraft: true, + username: true, + }, + }), + this.prisma.user.count({ where: combinedFilter }), + ]); + + return { + data: users, + meta: { + page: pagination.page, + perPage: pagination.limit, + totalItems: count, + totalPages: Math.ceil(count / pagination.limit), + }, + }; + } +}