diff --git a/.eslintrc.js b/.eslintrc.js index 259de13..567a5bc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,10 +6,7 @@ module.exports = { sourceType: 'module', }, plugins: ['@typescript-eslint/eslint-plugin'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], + extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], root: true, env: { node: true, @@ -21,5 +18,13 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], }, }; diff --git a/apps/service-auth/src/app.module.ts b/apps/service-auth/src/app.module.ts index c313244..3178ae6 100644 --- a/apps/service-auth/src/app.module.ts +++ b/apps/service-auth/src/app.module.ts @@ -4,6 +4,8 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AuthModule } from './auth/auth.module'; import { ClansModule } from './clans/clans.module'; +import { GuildsModule } from './guilds/guilds.module'; +import { LinksModule } from './links/links.module'; @Module({ imports: [ @@ -12,6 +14,8 @@ import { ClansModule } from './clans/clans.module'; RedisModule, ClansModule, AuthModule, + GuildsModule, + LinksModule, ], controllers: [], providers: [], diff --git a/apps/service-auth/src/auth/auth.controller.ts b/apps/service-auth/src/auth/auth.controller.ts index 97025ff..159e160 100644 --- a/apps/service-auth/src/auth/auth.controller.ts +++ b/apps/service-auth/src/auth/auth.controller.ts @@ -1,28 +1,34 @@ import { CurrentUser, JwtAuthGuard, JwtUser } from '@app/auth'; import { getAppHealth } from '@app/helper'; import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger'; import { AuthService } from './auth.service'; +import { LoginInput } from './dto/login.dto'; +@ApiTags('AUTH') @Controller('/auth') export class AuthController { constructor(private authService: AuthService) {} + @ApiExcludeEndpoint() @Get() ack() { return { message: `Hello from ${AuthController.name}` }; } + @ApiExcludeEndpoint() @Get('/health') stats() { return getAppHealth(AuthController.name); } @Post('/login') - async login(@Body('password') password: string) { - return this.authService.login(password); + async login(@Body() body: LoginInput) { + return this.authService.login(body.passkey); } @UseGuards(JwtAuthGuard) + @ApiBearerAuth() @Get('/status') getStatus(@CurrentUser() user: JwtUser) { return user; diff --git a/apps/service-auth/src/auth/dto/login.dto.ts b/apps/service-auth/src/auth/dto/login.dto.ts new file mode 100644 index 0000000..720383a --- /dev/null +++ b/apps/service-auth/src/auth/dto/login.dto.ts @@ -0,0 +1,6 @@ +import { IsNotEmpty } from 'class-validator'; + +export class LoginInput { + @IsNotEmpty() + passkey: string; +} diff --git a/apps/service-auth/src/clans/clans.controller.ts b/apps/service-auth/src/clans/clans.controller.ts new file mode 100644 index 0000000..327db03 --- /dev/null +++ b/apps/service-auth/src/clans/clans.controller.ts @@ -0,0 +1,14 @@ +import { JwtAuthGuard } from '@app/auth'; +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; + +@ApiTags('CLANS') +@ApiBearerAuth() +@Controller('/clans') +@UseGuards(JwtAuthGuard) +export class ClansController { + @Get() + getHello() { + return {}; + } +} diff --git a/apps/service-auth/src/clans/clans.module.ts b/apps/service-auth/src/clans/clans.module.ts index 079a6e0..19e76e6 100644 --- a/apps/service-auth/src/clans/clans.module.ts +++ b/apps/service-auth/src/clans/clans.module.ts @@ -1,4 +1,9 @@ import { Module } from '@nestjs/common'; +import { ClansService } from './clans.service'; +import { ClansController } from './clans.controller'; -@Module({}) +@Module({ + providers: [ClansService], + controllers: [ClansController], +}) export class ClansModule {} diff --git a/apps/service-auth/src/clans/clans.service.ts b/apps/service-auth/src/clans/clans.service.ts new file mode 100644 index 0000000..5a27cac --- /dev/null +++ b/apps/service-auth/src/clans/clans.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ClansService {} diff --git a/apps/service-auth/src/guilds/dto/update-clan-categories.dto.ts b/apps/service-auth/src/guilds/dto/update-clan-categories.dto.ts new file mode 100644 index 0000000..a6ac426 --- /dev/null +++ b/apps/service-auth/src/guilds/dto/update-clan-categories.dto.ts @@ -0,0 +1,32 @@ +import { IsInt, IsNotEmpty, ValidateNested } from 'class-validator'; + +export class ReorderCategoriesInput { + @IsNotEmpty() + _id: string; + + @IsNotEmpty() + name: string; + + @IsInt() + order: number; + + @ValidateNested({ each: true }) + clans: ReorderClansInput[]; +} + +export class ReorderClansInput { + @IsNotEmpty() + _id: string; + + @IsInt() + order: number; + + @IsNotEmpty() + tag: string; + + @IsNotEmpty() + name: string; + + @IsNotEmpty() + guildId: string; +} diff --git a/apps/service-auth/src/guilds/guilds.controller.ts b/apps/service-auth/src/guilds/guilds.controller.ts new file mode 100644 index 0000000..a5bec1e --- /dev/null +++ b/apps/service-auth/src/guilds/guilds.controller.ts @@ -0,0 +1,34 @@ +import { JwtAuthGuard } from '@app/auth'; +import { Body, Controller, Get, Param, Put, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiBody, ApiTags } from '@nestjs/swagger'; +import { ReorderCategoriesInput } from './dto/update-clan-categories.dto'; +import { GuildsService } from './guilds.service'; + +@ApiTags('GUILDS') +@Controller('/guilds') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +export class GuildsController { + constructor(private guildsService: GuildsService) {} + + @Get('/:guildId/clans') + getGuildMembers(@Param('guildId') guildId: string, @Query('q') q: string) { + return this.guildsService.getMembers(guildId, q); + } + + @Get('/:guildId/clans') + getClans(@Param('guildId') guildId: string) { + return this.guildsService.getClans(guildId); + } + + @Get('/:guildId/clans-and-categories') + getClansWithCategories(@Param('guildId') guildId: string) { + return this.guildsService.getClans(guildId); + } + + @Put('/:guildId/reorder-clans') + @ApiBody({ type: ReorderCategoriesInput, isArray: true }) + updateClansAndCategories(@Param('guildId') guildId: string, @Body() _: ReorderCategoriesInput[]) { + return this.guildsService.getClans(guildId); + } +} diff --git a/apps/service-auth/src/guilds/guilds.module.ts b/apps/service-auth/src/guilds/guilds.module.ts new file mode 100644 index 0000000..000a825 --- /dev/null +++ b/apps/service-auth/src/guilds/guilds.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { GuildsController } from './guilds.controller'; +import { GuildsService } from './guilds.service'; + +@Module({ + controllers: [GuildsController], + providers: [GuildsService], +}) +export class GuildsModule {} diff --git a/apps/service-auth/src/guilds/guilds.service.ts b/apps/service-auth/src/guilds/guilds.service.ts new file mode 100644 index 0000000..61ff3a9 --- /dev/null +++ b/apps/service-auth/src/guilds/guilds.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class GuildsService { + constructor() {} + + getMembers(guildId: string, q: string) { + return [guildId, q]; + } + + getClans(guildId: string) { + return [guildId]; + } +} diff --git a/apps/service-auth/src/links/dto/bulk-links.dto.ts b/apps/service-auth/src/links/dto/bulk-links.dto.ts new file mode 100644 index 0000000..5abd575 --- /dev/null +++ b/apps/service-auth/src/links/dto/bulk-links.dto.ts @@ -0,0 +1,7 @@ +import { ArrayMaxSize, IsString } from 'class-validator'; + +export class BulkLinksDto { + @IsString({ each: true }) + @ArrayMaxSize(1000) + input: string[]; +} diff --git a/apps/service-auth/src/links/dto/create-links.dto.ts b/apps/service-auth/src/links/dto/create-links.dto.ts new file mode 100644 index 0000000..7143297 --- /dev/null +++ b/apps/service-auth/src/links/dto/create-links.dto.ts @@ -0,0 +1,21 @@ +import { IsNotEmpty } from 'class-validator'; + +export class CreateLinkInput { + @IsNotEmpty() + name: string; + + @IsNotEmpty() + tag: string; + + @IsNotEmpty() + userId: string; + + @IsNotEmpty() + username: string; + + @IsNotEmpty() + displayName: string; + + @IsNotEmpty() + discriminator: string; +} diff --git a/apps/service-auth/src/links/dto/delete-link.dto.ts b/apps/service-auth/src/links/dto/delete-link.dto.ts new file mode 100644 index 0000000..3000ecb --- /dev/null +++ b/apps/service-auth/src/links/dto/delete-link.dto.ts @@ -0,0 +1,9 @@ +import { IsNotEmpty } from 'class-validator'; + +export class DeleteLinkInput { + @IsNotEmpty() + clanTag: string; + + @IsNotEmpty() + playerTag: string; +} diff --git a/apps/service-auth/src/links/links.controller.ts b/apps/service-auth/src/links/links.controller.ts new file mode 100644 index 0000000..414fcfb --- /dev/null +++ b/apps/service-auth/src/links/links.controller.ts @@ -0,0 +1,35 @@ +import { CurrentUser, JwtAuthGuard, JwtUser } from '@app/auth'; +import { Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { BulkLinksDto } from './dto/bulk-links.dto'; +import { CreateLinkInput } from './dto/create-links.dto'; +import { DeleteLinkInput } from './dto/delete-link.dto'; +import { LinksService } from './links.service'; + +@ApiTags('LINKS') +@Controller('/links') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +export class LinksController { + constructor(private linksService: LinksService) {} + + @Post('/') + async createLink(@Body() body: CreateLinkInput) { + return this.linksService.createLink(body); + } + + @Delete('/') + async deleteLink(@CurrentUser() user: JwtUser, @Body() body: DeleteLinkInput) { + return this.linksService.deleteLink(user.sub, body); + } + + @Get('/:userIdOrTag') + getLink(@Param('userIdOrTag') userIdOrTag: string) { + return this.linksService.getLink(userIdOrTag); + } + + @Post('/bulk') + getLinks(@Body() body: BulkLinksDto) { + return this.linksService.getLinks(body.input); + } +} diff --git a/apps/service-auth/src/links/links.module.ts b/apps/service-auth/src/links/links.module.ts new file mode 100644 index 0000000..b2569d6 --- /dev/null +++ b/apps/service-auth/src/links/links.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { LinksController } from './links.controller'; +import { LinksService } from './links.service'; + +@Module({ + controllers: [LinksController], + providers: [LinksService], +}) +export class LinksModule {} diff --git a/apps/service-auth/src/links/links.service.ts b/apps/service-auth/src/links/links.service.ts new file mode 100644 index 0000000..15bc1ff --- /dev/null +++ b/apps/service-auth/src/links/links.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; +import { CreateLinkInput } from './dto/create-links.dto'; +import { DeleteLinkInput } from './dto/delete-link.dto'; + +@Injectable() +export class LinksService { + createLink(input: CreateLinkInput) { + return input; + } + + deleteLink(userId: string, input: DeleteLinkInput) { + return { userId, input }; + } + + getLink(userIdOrTag: string) { + return [userIdOrTag]; + } + + getLinks(playerTags: string[]) { + return [playerTags]; + } +} diff --git a/apps/service-auth/src/main.ts b/apps/service-auth/src/main.ts index 0e393cf..4954bdd 100644 --- a/apps/service-auth/src/main.ts +++ b/apps/service-auth/src/main.ts @@ -1,10 +1,23 @@ -import { Logger } from '@nestjs/common'; +import { Logger, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); - const logger = new Logger('NestApplication'); + const logger = new Logger(AppModule.name); + + const config = new DocumentBuilder() + .setTitle('Service Auth API') + .setDescription('Public and private routes for the Service Auth') + .setVersion('1.0') + .addBearerAuth() + .addTag('AUTH') + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('/', app, document); + + app.useGlobalPipes(new ValidationPipe({ transform: true })); const port = process.env.PORT || 8081; await app.listen(port); diff --git a/libs/auth/src/decorators/roles.decorator.ts b/libs/auth/src/decorators/roles.decorator.ts index cec04f4..7d71da6 100644 --- a/libs/auth/src/decorators/roles.decorator.ts +++ b/libs/auth/src/decorators/roles.decorator.ts @@ -3,6 +3,7 @@ import { CustomDecorator, SetMetadata } from '@nestjs/common'; export enum Role { USER = 'user', ADMIN = 'admin', + VIEWER = 'viewer', } export const ROLES_KEY = 'roles'; diff --git a/nest-cli.json b/nest-cli.json index c33ebc9..8cce620 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -3,6 +3,9 @@ "collection": "@nestjs/schematics", "sourceRoot": "apps/service-auth/src", "compilerOptions": { + "plugins": [ + "@nestjs/swagger" + ], "deleteOutDir": true, "webpack": true, "tsConfigPath": "apps/service-auth/tsconfig.app.json" diff --git a/package-lock.json b/package-lock.json index 89a3952..30ac2d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,10 @@ "@nestjs/microservices": "^10.2.6", "@nestjs/passport": "^10.0.2", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.3.0", "clashofclans.js": "^3.1.5-dev.46123c0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "moment": "^2.29.4", "mongodb": "^6.1.0", "passport-jwt": "^4.0.1", @@ -1474,6 +1477,11 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.0.tgz", @@ -1666,6 +1674,25 @@ "npm": ">=6" } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/microservices": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-10.2.6.tgz", @@ -1824,6 +1851,38 @@ "node": ">=12" } }, + "node_modules/@nestjs/swagger": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.0.tgz", + "integrity": "sha512-zLkfKZ+ioYsIZ3dfv7Bj8YHnZMNAGWFUmx2ZDuLp/fBE4P8BSjB7hldzDueFXsmwaPL90v7lgyd82P+s7KME1Q==", + "dependencies": { + "@microsoft/tsdoc": "^0.14.2", + "@nestjs/mapped-types": "2.0.5", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "5.11.2" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.5.tgz", @@ -2326,6 +2385,11 @@ "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", "dev": true }, + "node_modules/@types/validator": { + "version": "13.11.9", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", + "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -2872,8 +2936,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -3439,6 +3502,21 @@ "node": ">=16.x" } }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -6137,7 +6215,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -6291,6 +6368,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.57", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.57.tgz", + "integrity": "sha512-OjsEd9y4LgcX+Ig09SbxWqcGESxliDDFNVepFhB9KEsQZTrnk3UdEU+cO0sW1APvLprHstQpS23OQpZ3bwxy6Q==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -8196,6 +8278,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz", + "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==" + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -8776,6 +8863,14 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 782a15a..684ab77 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,10 @@ "@nestjs/microservices": "^10.2.6", "@nestjs/passport": "^10.0.2", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.3.0", "clashofclans.js": "^3.1.5-dev.46123c0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "moment": "^2.29.4", "mongodb": "^6.1.0", "passport-jwt": "^4.0.1", @@ -94,4 +97,4 @@ "^@app/clash-client(|/.*)$": "/libs/clash-client/src/$1" } } -} \ No newline at end of file +}