From 4b413136ed187762b87b2939cd20d45b878d1ef3 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Wed, 1 Nov 2023 19:15:23 +0100 Subject: [PATCH 01/15] fix(swagger): replace entity usage in Swagger doc by DTOs --- src/exported | 2 +- src/modules/_mixin/dto/base-response.dto.ts | 17 --- src/modules/_mixin/dto/base.dto.ts | 21 +++ src/modules/_mixin/dto/error.dto.ts | 18 +++ .../_mixin/dto/message-response.dto.ts | 20 --- src/modules/_mixin/dto/message.dto.ts | 20 +++ src/modules/_mixin/entities/base.entity.ts | 4 +- src/modules/auth/auth.controller.ts | 31 ++-- src/modules/auth/auth.service.ts | 2 +- .../auth/dto/{token.dto.ts => get.dto.ts} | 4 +- .../auth/dto/{register.dto.ts => post.dto.ts} | 16 +- src/modules/auth/dto/sign-in.dto.ts | 15 -- src/modules/files/dto/get.dto.ts | 60 ++++++++ .../files/entities/file-visibility.entity.ts | 7 +- src/modules/files/entities/file.entity.ts | 15 +- src/modules/logs/dto/get.dto.ts | 62 ++++++++ src/modules/logs/entities/log.entity.ts | 19 +-- src/modules/logs/logs.controller.ts | 15 +- src/modules/logs/logs.service.ts | 20 +-- src/modules/permissions/dto/get.dto.ts | 35 +++++ src/modules/permissions/dto/patch.dto.ts | 4 +- src/modules/permissions/dto/post.dto.ts | 16 +- .../permissions/entities/permission.entity.ts | 10 +- .../permissions/permissions.controller.ts | 21 ++- .../permissions/permissions.service.ts | 17 +-- src/modules/promotions/dto/get.dto.ts | 27 ++++ src/modules/promotions/dto/promotion.dto.ts | 18 --- .../entities/promotion-picture.entity.ts | 6 +- .../promotions/entities/promotion.entity.ts | 8 +- .../promotions/promotions.controller.ts | 43 +++--- src/modules/promotions/promotions.service.ts | 21 ++- src/modules/roles/dto/get.dto.ts | 31 ++++ src/modules/roles/dto/patch.dto.ts | 12 +- src/modules/roles/dto/post.dto.ts | 4 +- src/modules/roles/dto/users.dto.ts | 10 -- .../roles/entities/role-expiration.entity.ts | 3 - src/modules/roles/entities/role.entity.ts | 14 +- src/modules/roles/roles.controller.ts | 39 ++--- src/modules/roles/roles.service.ts | 12 +- .../entities/subscription.entity.ts | 28 ---- .../controllers/users-data.controller.ts | 62 ++++---- .../controllers/users-files.controller.ts | 49 +++--- src/modules/users/dto/base-user.dto.ts | 12 +- src/modules/users/dto/get.dto.ts | 140 +++++++++++++++++- src/modules/users/dto/patch.dto.ts | 104 +------------ .../users/entities/user-banner.entity.ts | 6 +- .../users/entities/user-picture.entity.ts | 6 +- .../users/entities/user-visibility.entity.ts | 15 +- src/modules/users/entities/user.entity.ts | 36 +---- .../users/services/users-data.service.ts | 53 ++++--- .../users/services/users-files.service.ts | 25 ++-- tests/e2e/auth.e2e-spec.ts | 2 +- tests/e2e/logs.e2e-spec.ts | 2 +- tests/e2e/permissions.e2e-spec.ts | 2 +- tests/e2e/promotions.e2e-spec.ts | 2 +- tests/e2e/users/users-data.e2e-spec.ts | 2 +- tests/e2e/users/users-files.e2e-spec.ts | 2 +- 57 files changed, 690 insertions(+), 577 deletions(-) delete mode 100644 src/modules/_mixin/dto/base-response.dto.ts create mode 100644 src/modules/_mixin/dto/base.dto.ts create mode 100644 src/modules/_mixin/dto/error.dto.ts delete mode 100644 src/modules/_mixin/dto/message-response.dto.ts create mode 100644 src/modules/_mixin/dto/message.dto.ts rename src/modules/auth/dto/{token.dto.ts => get.dto.ts} (71%) rename src/modules/auth/dto/{register.dto.ts => post.dto.ts} (56%) delete mode 100644 src/modules/auth/dto/sign-in.dto.ts create mode 100644 src/modules/files/dto/get.dto.ts create mode 100644 src/modules/logs/dto/get.dto.ts create mode 100644 src/modules/permissions/dto/get.dto.ts create mode 100644 src/modules/promotions/dto/get.dto.ts delete mode 100644 src/modules/promotions/dto/promotion.dto.ts create mode 100644 src/modules/roles/dto/get.dto.ts delete mode 100644 src/modules/roles/dto/users.dto.ts delete mode 100644 src/modules/subscription/entities/subscription.entity.ts diff --git a/src/exported b/src/exported index 5d5f4829..4f920643 160000 --- a/src/exported +++ b/src/exported @@ -1 +1 @@ -Subproject commit 5d5f482951956602535fde7e5498fc6502b18731 +Subproject commit 4f920643fef209a20c927e0d0970466e8c6fba4a diff --git a/src/modules/_mixin/dto/base-response.dto.ts b/src/modules/_mixin/dto/base-response.dto.ts deleted file mode 100644 index 34fe5ac6..00000000 --- a/src/modules/_mixin/dto/base-response.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -import { BaseResponseDto } from '#types/api'; - -/** - * Base response DTO class - */ -export abstract class BaseResponseDTO implements BaseResponseDto { - @ApiProperty({ minimum: 1 }) - id: number; - - @ApiProperty() - updated: Date; - - @ApiProperty() - created: Date; -} diff --git a/src/modules/_mixin/dto/base.dto.ts b/src/modules/_mixin/dto/base.dto.ts new file mode 100644 index 00000000..3aad392f --- /dev/null +++ b/src/modules/_mixin/dto/base.dto.ts @@ -0,0 +1,21 @@ +import type { IBaseResponseDTO } from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsDate, IsInt } from 'class-validator'; + +/** + * Base response DTO class + */ +export abstract class BaseResponseDTO implements IBaseResponseDTO { + @ApiProperty({ minimum: 1 }) + @IsInt() + id: number; + + @ApiProperty() + @IsDate() + updated: Date; + + @ApiProperty() + @IsDate() + created: Date; +} diff --git a/src/modules/_mixin/dto/error.dto.ts b/src/modules/_mixin/dto/error.dto.ts new file mode 100644 index 00000000..65446298 --- /dev/null +++ b/src/modules/_mixin/dto/error.dto.ts @@ -0,0 +1,18 @@ +import type { IErrorResponseDTO } from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsInt } from 'class-validator'; + +export class ErrorResponseDTO implements IErrorResponseDTO { + @ApiProperty() + @IsString() + error: string; + + @ApiProperty() + @IsString() + message: string; + + @ApiProperty({ example: 400 }) + @IsInt() + statusCode: number; +} diff --git a/src/modules/_mixin/dto/message-response.dto.ts b/src/modules/_mixin/dto/message-response.dto.ts deleted file mode 100644 index e87c2488..00000000 --- a/src/modules/_mixin/dto/message-response.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { MessageResponseDto } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber, IsString } from 'class-validator'; - -/** - * Message response DTO class (used to send a message to the client) - * -> Mainly used for DELETE requests // TODO: Is a text message is really needed for DELETE requests? - * - * @example { message: 'User successfully deleted', status_code: 200 } - */ -export class MessageResponseDTO implements MessageResponseDto { - @ApiProperty() - @IsString() - message: string; - - @ApiProperty({ example: 200 }) - @IsNumber() - status_code: number; -} diff --git a/src/modules/_mixin/dto/message.dto.ts b/src/modules/_mixin/dto/message.dto.ts new file mode 100644 index 00000000..b606869e --- /dev/null +++ b/src/modules/_mixin/dto/message.dto.ts @@ -0,0 +1,20 @@ +import type { IMessageResponseDTO } from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt, IsString } from 'class-validator'; + +/** + * Message response DTO class (used to send a message to the client) + * -> Mainly used for DELETE requests + * + * @example { message: 'User successfully deleted', status_code: 200 } + */ +export class MessageResponseDTO implements IMessageResponseDTO { + @ApiProperty() + @IsString() + message: string; + + @ApiProperty({ example: 200 }) + @IsInt() + statusCode: number; +} diff --git a/src/modules/_mixin/entities/base.entity.ts b/src/modules/_mixin/entities/base.entity.ts index ab84c186..c0f2c68a 100644 --- a/src/modules/_mixin/entities/base.entity.ts +++ b/src/modules/_mixin/entities/base.entity.ts @@ -1,4 +1,4 @@ -import type { BaseEntity as BEI } from '#types/api'; +import type { IBaseResponseDTO } from '#types/api'; import { BaseEntity as BE, Entity, PrimaryKey, Property } from '@mikro-orm/core'; import { ApiProperty } from '@nestjs/swagger'; @@ -8,7 +8,7 @@ import { ApiProperty } from '@nestjs/swagger'; * - Contains the primary key, the creation and update dates */ @Entity({ abstract: true }) -export abstract class BaseEntity extends BE implements BEI { +export abstract class BaseEntity extends BE { @PrimaryKey() @ApiProperty({ minimum: 1 }) id: number; diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 8c08db8b..011e3e37 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -11,16 +11,16 @@ import { } from '@nestjs/swagger'; import { z } from 'zod'; -import { MessageResponseDTO } from '@modules/_mixin/dto/message-response.dto'; +import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; +import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; import { TranslateService } from '@modules/translate/translate.service'; import { User } from '@modules/users/entities/user.entity'; import { UsersDataService } from '@modules/users/services/users-data.service'; import { validate } from '@utils/validate'; import { AuthService } from './auth.service'; -import { UserPostDTO } from './dto/register.dto'; -import { UserSignInDTO } from './dto/sign-in.dto'; -import { TokenDTO } from './dto/token.dto'; +import { TokenDTO } from './dto/get.dto'; +import { UserPostDTO, SignInDTO } from './dto/post.dto'; @ApiTags('Authentification') @Controller('auth') @@ -33,11 +33,11 @@ export class AuthController { @Post('login') @ApiOperation({ summary: 'Sign in a user with email and password' }) - @ApiUnauthorizedResponse({ description: 'Unauthorized, password invalid' }) - @ApiForbiddenResponse({ description: 'Forbidden, email not verified' }) - @ApiNotFoundResponse({ description: 'User not found' }) + @ApiUnauthorizedResponse({ description: 'Unauthorized, password invalid', type: ErrorResponseDTO }) + @ApiForbiddenResponse({ description: 'Forbidden, email not verified', type: ErrorResponseDTO }) + @ApiNotFoundResponse({ description: 'User not found', type: ErrorResponseDTO }) @ApiOkResponse({ description: 'OK', type: TokenDTO }) - async login(@Body() signInDto: UserSignInDTO): Promise { + async login(@Body() signInDto: SignInDTO) { const schema = z .object({ password: z.string(), @@ -52,9 +52,9 @@ export class AuthController { @Post('register') @ApiOperation({ summary: 'Register a new user' }) - @ApiOkResponse({ description: 'User created', type: User }) - @ApiBadRequestResponse({ description: 'Bad request, invalid fields' }) - async register(@Body() registerDto: UserPostDTO): Promise { + @ApiOkResponse({ description: 'User created', type: MessageResponseDTO }) + @ApiBadRequestResponse({ description: 'Bad request, invalid fields', type: ErrorResponseDTO }) + async register(@Body() registerDto: UserPostDTO) { const schema = z .object({ password: z.string(), @@ -74,9 +74,12 @@ export class AuthController { @ApiParam({ name: 'user_id', type: Number }) @ApiParam({ name: 'token', type: String }) @ApiOperation({ summary: 'Validate a user account and redirect after' }) - @ApiNotFoundResponse({ description: 'User not found' }) - @ApiBadRequestResponse({ description: 'Bad request, missing id/token or email already verified' }) - @ApiUnauthorizedResponse({ description: 'Unauthorized, invalid token' }) + @ApiNotFoundResponse({ description: 'User not found', type: ErrorResponseDTO }) + @ApiBadRequestResponse({ + description: 'Missing id/token or email already verified', + type: ErrorResponseDTO, + }) + @ApiUnauthorizedResponse({ description: 'Unauthorized, invalid token', type: ErrorResponseDTO }) @ApiOkResponse({ description: 'OK', type: MessageResponseDTO }) async verifyEmailAndRedirect(@Param('user_id') user_id: number, @Param('token') token: string) { validate(z.coerce.number().int().min(1), user_id, this.t.Errors.Id.Invalid(User, user_id)); diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 7ab02553..6a3b0f73 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -10,7 +10,7 @@ import { TranslateService } from '@modules/translate/translate.service'; import { User } from '@modules/users/entities/user.entity'; import { UsersDataService } from '@modules/users/services/users-data.service'; -import { TokenDTO } from './dto/token.dto'; +import { TokenDTO } from './dto/get.dto'; @Injectable() export class AuthService { diff --git a/src/modules/auth/dto/token.dto.ts b/src/modules/auth/dto/get.dto.ts similarity index 71% rename from src/modules/auth/dto/token.dto.ts rename to src/modules/auth/dto/get.dto.ts index 38efbd95..ffc5e07f 100644 --- a/src/modules/auth/dto/token.dto.ts +++ b/src/modules/auth/dto/get.dto.ts @@ -1,9 +1,9 @@ -import type { UserTokenDto } from '#types/api'; +import type { ITokenDTO } from '#types/api'; import { ApiProperty } from '@nestjs/swagger'; import { IsInt, IsString } from 'class-validator'; -export class TokenDTO implements UserTokenDto { +export class TokenDTO implements ITokenDTO { @ApiProperty({ example: 'xxxxx.yyyyy.zzzzz' }) @IsString() token: string; diff --git a/src/modules/auth/dto/register.dto.ts b/src/modules/auth/dto/post.dto.ts similarity index 56% rename from src/modules/auth/dto/register.dto.ts rename to src/modules/auth/dto/post.dto.ts index 7f3fc10b..972a5e57 100644 --- a/src/modules/auth/dto/register.dto.ts +++ b/src/modules/auth/dto/post.dto.ts @@ -1,10 +1,10 @@ import type { email } from '#types'; -import type { UserPostDto } from '#types/api'; +import type { ICreateUserDTO, ICreateUserByAdminDTO, ISignInDTO } from '#types/api'; import { ApiProperty } from '@nestjs/swagger'; import { IsDate, IsEmail, IsString, IsStrongPassword } from 'class-validator'; -export class UserPostByAdminDTO implements Omit { +export class CreateUserDTO implements Omit { @ApiProperty({ example: 'example@domain.com' }) @IsEmail() email: email; @@ -22,8 +22,18 @@ export class UserPostByAdminDTO implements Omit { last_name: string; } -export class UserPostDTO extends UserPostByAdminDTO implements UserPostDto { +export class UserPostDTO extends CreateUserDTO implements ICreateUserByAdminDTO { @ApiProperty({ example: 'password' }) @IsStrongPassword() password: string; } + +export class SignInDTO implements ISignInDTO { + @ApiProperty({ type: String }) + @IsString() + email: email; + + @ApiProperty({ example: 'password' }) + @IsString() + password: string; +} diff --git a/src/modules/auth/dto/sign-in.dto.ts b/src/modules/auth/dto/sign-in.dto.ts deleted file mode 100644 index b68b86c2..00000000 --- a/src/modules/auth/dto/sign-in.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { email } from '#types'; -import type { UserSignInDto } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -export class UserSignInDTO implements UserSignInDto { - @ApiProperty({ type: String }) - @IsString() - email: email; - - @ApiProperty({ example: 'password' }) - @IsString() - password: string; -} diff --git a/src/modules/files/dto/get.dto.ts b/src/modules/files/dto/get.dto.ts new file mode 100644 index 00000000..f6b152ac --- /dev/null +++ b/src/modules/files/dto/get.dto.ts @@ -0,0 +1,60 @@ +import type { IFileGetDTO, IFileVisibilityGroupGetDTO } from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsDate, IsInt, IsString } from 'class-validator'; + +export class FileGetDTO implements IFileGetDTO { + @ApiProperty() + @IsInt() + id: number; + + @ApiProperty() + @IsDate() + updated: Date; + + @ApiProperty() + @IsDate() + created: Date; + + @ApiProperty() + @IsString() + filename: string; + + @ApiProperty() + @IsString() + mimetype: string; + + @ApiProperty() + @IsString() + path: string; + + @ApiProperty() + @IsInt() + size: number; + + @ApiProperty({ required: false }) + @IsInt() + visibility?: number; + + @ApiProperty() + @IsString() + description?: string; +} + +export class FileVisibilityGroupGetDTO implements IFileVisibilityGroupGetDTO { + @ApiProperty() + @IsString() // TODO : verify uppercase + name: Uppercase; + + @ApiProperty() + @IsString() + description: string; + + @ApiProperty() + @IsInt() + users: number; + + @ApiProperty() + @IsInt() + files: number; +} diff --git a/src/modules/files/entities/file-visibility.entity.ts b/src/modules/files/entities/file-visibility.entity.ts index 0c36f0c0..5a9347fc 100644 --- a/src/modules/files/entities/file-visibility.entity.ts +++ b/src/modules/files/entities/file-visibility.entity.ts @@ -1,20 +1,15 @@ -import type { FileVisibilityGroupEntity } from '#types/api'; - import { Collection, Entity, ManyToMany, OneToMany, Property } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; import { BaseEntity } from '@modules/_mixin/entities/base.entity'; import { File } from '@modules/files/entities/file.entity'; import { User } from '@modules/users/entities/user.entity'; @Entity({ tableName: 'files_visibility_groups' }) -export class FileVisibilityGroup extends BaseEntity implements FileVisibilityGroupEntity { +export class FileVisibilityGroup extends BaseEntity { @Property() - @ApiProperty() name: Uppercase; @Property() - @ApiProperty() description: string; //* Note: Used the 'string' version of the entity name to avoid circular dependency issues. diff --git a/src/modules/files/entities/file.entity.ts b/src/modules/files/entities/file.entity.ts index 659be743..1f251f73 100644 --- a/src/modules/files/entities/file.entity.ts +++ b/src/modules/files/entities/file.entity.ts @@ -1,10 +1,12 @@ -import type { FileEntity as FE } from '#types/api'; - import { Entity, ManyToOne, Property } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; import { BaseEntity } from '@modules/_mixin/entities/base.entity'; import { FileVisibilityGroup } from '@modules/files/entities/file-visibility.entity'; +import { PromotionPicture } from '@modules/promotions/entities/promotion-picture.entity'; +import { UserBanner } from '@modules/users/entities/user-banner.entity'; +import { UserPicture } from '@modules/users/entities/user-picture.entity'; + +export type FileKind = UserPicture | UserBanner | PromotionPicture; @Entity({ tableName: 'files', @@ -16,28 +18,23 @@ import { FileVisibilityGroup } from '@modules/files/entities/file-visibility.ent }, abstract: true, }) -export abstract class File extends BaseEntity implements FE { +export abstract class File extends BaseEntity { @Property() - @ApiProperty() filename: string; @Property() - @ApiProperty() mimetype: string; @Property({ hidden: true }) path: string; @Property() - @ApiProperty() size: number; @ManyToOne(() => FileVisibilityGroup, { nullable: true, default: null }) - @ApiProperty({ type: Number, minimum: 1 }) visibility?: FileVisibilityGroup; @Property({ nullable: true, default: null }) - @ApiProperty({ type: String, nullable: true }) description?: string; abstract get owner(): T; diff --git a/src/modules/logs/dto/get.dto.ts b/src/modules/logs/dto/get.dto.ts new file mode 100644 index 00000000..8a386083 --- /dev/null +++ b/src/modules/logs/dto/get.dto.ts @@ -0,0 +1,62 @@ +import type { ILogDTO } from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt, IsString } from 'class-validator'; + +export class LogDTO implements ILogDTO { + @ApiProperty() + @IsInt() + user: number; + + @ApiProperty() + @IsString() + action: string; + + @ApiProperty() + @IsString() + ip: string; + + @ApiProperty() + @IsString() + user_agent: string; + + @ApiProperty() + @IsString() + route: string; + + @ApiProperty() + @IsString() + method: string; + + @ApiProperty() + @IsString() + body: string; + + @ApiProperty() + @IsString() + query: string; + + @ApiProperty() + @IsString() + params: string; + + @ApiProperty({ required: false }) + @IsString() + response?: string; + + @ApiProperty({ required: false }) + @IsInt() + status_code?: number; + + @ApiProperty({ required: false }) + @IsString() + error?: string; + + @ApiProperty({ required: false }) + @IsString() + error_stack?: string; + + @ApiProperty({ required: false }) + @IsString() + error_message?: string; +} diff --git a/src/modules/logs/entities/log.entity.ts b/src/modules/logs/entities/log.entity.ts index a8819478..0b68d4e1 100644 --- a/src/modules/logs/entities/log.entity.ts +++ b/src/modules/logs/entities/log.entity.ts @@ -1,66 +1,49 @@ -import type { LogEntity } from '#types/api'; - import { Entity, ManyToOne, Property } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; import { BaseEntity } from '@modules/_mixin/entities/base.entity'; import { User } from '@modules/users/entities/user.entity'; @Entity({ tableName: 'users_logs' }) -export class Log extends BaseEntity implements LogEntity { +export class Log extends BaseEntity { @ManyToOne() - @ApiProperty({ type: Number }) user: User; @Property() - @ApiProperty() action: string; @Property() - @ApiProperty() ip: string; @Property() - @ApiProperty() user_agent: string; @Property() - @ApiProperty() route: string; @Property() - @ApiProperty() method: string; @Property() - @ApiProperty() body: string; @Property() - @ApiProperty() query: string; @Property() - @ApiProperty() params: string; @Property() - @ApiProperty({ required: false }) response?: string; @Property() - @ApiProperty({ required: false }) status_code?: number; @Property({ nullable: true }) - @ApiProperty({ required: false }) error?: string; @Property({ nullable: true }) - @ApiProperty({ required: false }) error_stack?: string; @Property({ nullable: true }) - @ApiProperty({ required: false }) error_message?: string; } diff --git a/src/modules/logs/logs.controller.ts b/src/modules/logs/logs.controller.ts index ad6d5aa1..08f27d47 100644 --- a/src/modules/logs/logs.controller.ts +++ b/src/modules/logs/logs.controller.ts @@ -11,7 +11,8 @@ import { } from '@nestjs/swagger'; import { z } from 'zod'; -import { MessageResponseDTO } from '@modules/_mixin/dto/message-response.dto'; +import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; +import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; @@ -20,7 +21,7 @@ import { TranslateService } from '@modules/translate/translate.service'; import { User } from '@modules/users/entities/user.entity'; import { validate } from '@utils/validate'; -import { Log } from './entities/log.entity'; +import { LogDTO } from './dto/get.dto'; import { LogsService } from './logs.service'; @Controller('logs') @@ -33,9 +34,9 @@ export class LogsController { @Get('user/:user_id') @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('user_id', ['CAN_READ_LOGS_OF_USER']) - @ApiNotFoundResponse({ description: 'User not found' }) - @ApiBadRequestResponse({ description: 'Invalid user ID' }) - @ApiOkResponse({ description: 'User logs retrieved', type: [Log] }) + @ApiNotFoundResponse({ description: 'User not found', type: ErrorResponseDTO }) + @ApiBadRequestResponse({ description: 'Invalid user ID', type: ErrorResponseDTO }) + @ApiOkResponse({ description: 'User logs retrieved', type: [LogDTO] }) @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOperation({ summary: 'Get all logs of a user' }) getUserLogs(@Param('user_id') id: number) { @@ -47,8 +48,8 @@ export class LogsController { @Delete('user/:user_id') @UseGuards(PermissionGuard) @GuardPermissions('CAN_DELETE_LOGS_OF_USER') - @ApiNotFoundResponse({ description: 'User not found' }) - @ApiBadRequestResponse({ description: 'Invalid user ID' }) + @ApiNotFoundResponse({ description: 'User not found', type: ErrorResponseDTO }) + @ApiBadRequestResponse({ description: 'Invalid user ID', type: ErrorResponseDTO }) @ApiOkResponse({ description: 'User logs deleted', type: MessageResponseDTO }) @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOperation({ summary: 'Delete all logs of a user' }) diff --git a/src/modules/logs/logs.service.ts b/src/modules/logs/logs.service.ts index 02e5ac62..fb36ed1f 100644 --- a/src/modules/logs/logs.service.ts +++ b/src/modules/logs/logs.service.ts @@ -3,17 +3,13 @@ import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { TranslateService } from '@modules/translate/translate.service'; -import { UsersDataService } from '@modules/users/services/users-data.service'; +import { LogDTO } from './dto/get.dto'; import { Log } from './entities/log.entity'; @Injectable() export class LogsService { - constructor( - private readonly orm: MikroORM, - private readonly t: TranslateService, - private readonly usersService: UsersDataService, - ) {} + constructor(private readonly orm: MikroORM, private readonly t: TranslateService) {} /** * Remove all logs that are older than 2 months each day at 7am @@ -24,18 +20,12 @@ export class LogsService { await this.orm.em.nativeDelete(Log, { created: { $lt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 60) } }); } - async getUserLogs(id: number): Promise<(Omit & { user: number })[]> { - const user = await this.usersService.findOne(id, false); - const logs = (await user.logs.loadItems()).map((log) => ({ ...log, user: log.user.id })); - - return logs; + async getUserLogs(id: number): Promise { + return (await this.orm.em.find(Log, { user: id })).map((log) => ({ ...log, user: log.user.id })); } async deleteUserLogs(id: number) { - const user = await this.usersService.findOne(id, false); - await user.logs.init(); - user.logs.removeAll(); - + await this.orm.em.nativeDelete(Log, { user: id }); return { message: this.t.Success.Entity.Deleted(Log), statusCode: 200 }; } } diff --git a/src/modules/permissions/dto/get.dto.ts b/src/modules/permissions/dto/get.dto.ts new file mode 100644 index 00000000..a7c201d4 --- /dev/null +++ b/src/modules/permissions/dto/get.dto.ts @@ -0,0 +1,35 @@ +import type { IPermissionGetDTO, IPermissionsOfRoleDTO, PERMISSION_NAMES } from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt, IsArray, IsDate, IsBoolean } from 'class-validator'; + +import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; +import { BaseResponseDTO } from '@modules/_mixin/dto/base.dto'; + +export class PermissionsOfRoleDTO implements IPermissionsOfRoleDTO { + @ApiProperty() + @IsInt() + id: number; + + @ApiProperty({ enum: PERMISSIONS_NAMES, isArray: true }) + @IsArray() + permissions: PERMISSION_NAMES[]; +} + +export class PermissionGetDTO extends BaseResponseDTO implements IPermissionGetDTO { + @ApiProperty({ enum: PERMISSIONS_NAMES }) + @IsArray() + name: PERMISSION_NAMES; + + @ApiProperty() + @IsInt() + user: number; + + @ApiProperty() + @IsBoolean() + revoked: boolean; + + @ApiProperty() + @IsDate() + expires: Date; +} diff --git a/src/modules/permissions/dto/patch.dto.ts b/src/modules/permissions/dto/patch.dto.ts index 5ffcbc27..3b5721a2 100644 --- a/src/modules/permissions/dto/patch.dto.ts +++ b/src/modules/permissions/dto/patch.dto.ts @@ -1,11 +1,11 @@ -import type { PERMISSION_NAMES, PermissionPatchDto } from '#types/api'; +import type { PERMISSION_NAMES, IPermissionPatchDTO } from '#types/api'; import { ApiProperty } from '@nestjs/swagger'; import { IsInt, IsString, IsDate, IsBoolean } from 'class-validator'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -export class PermissionPatchDTO implements PermissionPatchDto { +export class PermissionPatchDTO implements IPermissionPatchDTO { @ApiProperty({ required: true, minimum: 1 }) @IsInt() id: number; diff --git a/src/modules/permissions/dto/post.dto.ts b/src/modules/permissions/dto/post.dto.ts index d801beee..7d3ee494 100644 --- a/src/modules/permissions/dto/post.dto.ts +++ b/src/modules/permissions/dto/post.dto.ts @@ -1,11 +1,11 @@ -import type { PERMISSION_NAMES, PermissionsPostDto } from '#types/api'; +import type { PERMISSION_NAMES, PermissionPostDto } from '#types/api'; import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsDate, IsInt, IsString } from 'class-validator'; +import { IsDate, IsInt, IsString } from 'class-validator'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -export class PermissionPostDTO implements PermissionsPostDto { +export class PermissionPostDTO implements PermissionPostDto { @ApiProperty() @IsInt() id: number; @@ -18,13 +18,3 @@ export class PermissionPostDTO implements PermissionsPostDto { @IsDate() expires: Date; } - -export class RolePermissionsDto implements RolePermissionsDto { - @ApiProperty() - @IsInt() - id: number; - - @ApiProperty({ enum: PERMISSIONS_NAMES, isArray: true }) - @IsArray() - permissions: PERMISSION_NAMES[]; -} diff --git a/src/modules/permissions/entities/permission.entity.ts b/src/modules/permissions/entities/permission.entity.ts index 7e55f7c6..924cfdc5 100644 --- a/src/modules/permissions/entities/permission.entity.ts +++ b/src/modules/permissions/entities/permission.entity.ts @@ -1,27 +1,21 @@ -import type { PermissionEntity, PERMISSION_NAMES } from '#types/api'; +import type { PERMISSION_NAMES } from '#types/api'; import { Entity, ManyToOne, Property } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; -import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; import { BaseEntity } from '@modules/_mixin/entities/base.entity'; import { User } from '@modules/users/entities/user.entity'; @Entity({ tableName: 'permissions' }) -export class Permission extends BaseEntity implements PermissionEntity { +export class Permission extends BaseEntity { @Property() - @ApiProperty({ enum: PERMISSIONS_NAMES }) name: PERMISSION_NAMES; @Property({ name: 'is_revoked', default: false }) - @ApiProperty({ type: Boolean }) revoked: boolean; @Property({ name: 'expires_at' }) - @ApiProperty() expires: Date; @ManyToOne(() => User, { onDelete: 'cascade', joinColumn: 'user_id' }) - @ApiProperty({ type: Number, minimum: 1 }) user: User; } diff --git a/src/modules/permissions/permissions.controller.ts b/src/modules/permissions/permissions.controller.ts index b144c3c4..2da22ad7 100644 --- a/src/modules/permissions/permissions.controller.ts +++ b/src/modules/permissions/permissions.controller.ts @@ -1,5 +1,3 @@ -import type { PermissionEntity } from '#types/api'; - import { Body, Controller, Get, Param, Post, UseGuards, Patch } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { @@ -13,6 +11,7 @@ import { } from '@nestjs/swagger'; import { z } from 'zod'; +import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; @@ -21,9 +20,9 @@ import { TranslateService } from '@modules/translate/translate.service'; import { User } from '@modules/users/entities/user.entity'; import { validate } from '@utils/validate'; +import { PermissionGetDTO } from './dto/get.dto'; import { PermissionPatchDTO } from './dto/patch.dto'; import { PermissionPostDTO } from './dto/post.dto'; -import { Permission } from './entities/permission.entity'; import { PermissionsService } from './permissions.service'; @Controller('permissions') @@ -37,10 +36,10 @@ export class PermissionsController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_PERMISSIONS_OF_USER') @ApiOperation({ summary: 'Add a permission to a user' }) - @ApiOkResponse({ description: 'The added permission', type: Permission }) - @ApiNotFoundResponse({ description: 'User not found' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - addToUser(@Body() body: PermissionPostDTO): Promise> { + @ApiOkResponse({ description: 'The added permission', type: PermissionGetDTO }) + @ApiNotFoundResponse({ description: 'User not found', type: ErrorResponseDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + addToUser(@Body() body: PermissionPostDTO) { const schema = z .object({ expires: z.string().datetime(), @@ -57,9 +56,9 @@ export class PermissionsController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_PERMISSIONS_OF_USER') @ApiOperation({ summary: 'Edit permission of a user' }) - @ApiNotFoundResponse({ description: 'User not found' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiOkResponse({ description: 'The modified user permission', type: Permission }) + @ApiNotFoundResponse({ description: 'User not found', type: ErrorResponseDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiOkResponse({ description: 'The modified user permission', type: PermissionGetDTO }) editPermissionFromUser(@Body() body: PermissionPatchDTO) { const schema = z .object({ @@ -84,7 +83,7 @@ export class PermissionsController { @ApiOperation({ summary: 'Get all permissions of a user (active, revoked and expired)' }) @ApiNotFoundResponse({ description: 'User not found' }) @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiOkResponse({ description: 'User permission(s) retrieved', type: [Permission] }) + @ApiOkResponse({ description: 'User permission(s) retrieved', type: [PermissionGetDTO] }) @ApiParam({ name: 'user_id', description: 'The user ID' }) getUserPermissions(@Param('user_id') id: number) { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); diff --git a/src/modules/permissions/permissions.service.ts b/src/modules/permissions/permissions.service.ts index 2d733be0..3065ba01 100644 --- a/src/modules/permissions/permissions.service.ts +++ b/src/modules/permissions/permissions.service.ts @@ -1,5 +1,3 @@ -import type { PermissionEntity } from '#types/api'; - import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; @@ -7,6 +5,7 @@ import { Cron } from '@nestjs/schedule'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; import { TranslateService } from '@modules/translate/translate.service'; +import { PermissionGetDTO } from './dto/get.dto'; import { PermissionPatchDTO } from './dto/patch.dto'; import { PermissionPostDTO } from './dto/post.dto'; import { Permission } from './entities/permission.entity'; @@ -37,11 +36,11 @@ export class PermissionsService { /** * Add a permission to a user - * @param {PermissionPatchDTO} data The permission data to add - * @returns {Promise>} The created permission + * @param data The permission data to add + * @returns The created permission */ @CreateRequestContext() - async addPermissionToUser(data: PermissionPostDTO): Promise> { + async addPermissionToUser(data: PermissionPostDTO): Promise { // Check if the permission is valid if (!PERMISSIONS_NAMES.includes(data.permission)) throw new BadRequestException(this.t.Errors.Permission.Invalid(data.permission)); @@ -69,7 +68,7 @@ export class PermissionsService { // Save it & return it await this.orm.em.persistAndFlush(permission); - return { ...permission, user: user.id }; + return { ...permission, user: user.id } as PermissionGetDTO; } /** @@ -86,11 +85,11 @@ export class PermissionsService { } @CreateRequestContext() - async editPermissionOfUser(data: PermissionPatchDTO): Promise { + async editPermissionOfUser(data: PermissionPatchDTO): Promise { const user = await this.orm.em.findOne(User, { id: data.user_id }); if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, data.user_id)); - const perm = await this.orm.em.findOne(Permission, { id: data.id, user }); + const perm = await this.orm.em.findOne(Permission, { id: data.id, user: data.user_id }); if (!perm) throw new NotFoundException(this.t.Errors.Permission.NotFoundOnUser(data.name, user.full_name)); if (data.name) perm.name = data.name; @@ -98,6 +97,6 @@ export class PermissionsService { if (data.revoked !== undefined) perm.revoked = data.revoked; await this.orm.em.persistAndFlush(perm); - return perm; + return { ...perm, user: user.id }; } } diff --git a/src/modules/promotions/dto/get.dto.ts b/src/modules/promotions/dto/get.dto.ts new file mode 100644 index 00000000..111cbbd4 --- /dev/null +++ b/src/modules/promotions/dto/get.dto.ts @@ -0,0 +1,27 @@ +import type { IPromotionPictureResponseDTO, IPromotionResponseDTO } from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt } from 'class-validator'; + +import { BaseResponseDTO } from '@modules/_mixin/dto/base.dto'; +import { FileGetDTO } from '@modules/files/dto/get.dto'; + +export class PromotionResponseDTO extends BaseResponseDTO implements IPromotionResponseDTO { + @ApiProperty() + @IsInt() + number: number; + + @ApiProperty() + @IsInt() + users: number; + + @ApiProperty({ required: false }) + @IsInt() + picture?: number; +} + +export class PromotionPictureResponseDTO extends FileGetDTO implements IPromotionPictureResponseDTO { + @ApiProperty() + @IsInt() + picture_promotion: number; +} diff --git a/src/modules/promotions/dto/promotion.dto.ts b/src/modules/promotions/dto/promotion.dto.ts deleted file mode 100644 index a5553667..00000000 --- a/src/modules/promotions/dto/promotion.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { PromotionResponseDto } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; - -import { BaseResponseDTO } from '@modules/_mixin/dto/base-response.dto'; - -import { PromotionPicture } from '../entities/promotion-picture.entity'; - -export class PromotionResponseDTO extends BaseResponseDTO implements PromotionResponseDto { - @ApiProperty() - number: number; - - @ApiProperty() - users: number; - - @ApiProperty({ required: false }) - picture?: PromotionPicture; -} diff --git a/src/modules/promotions/entities/promotion-picture.entity.ts b/src/modules/promotions/entities/promotion-picture.entity.ts index 2f7fdc8c..dc1035f5 100644 --- a/src/modules/promotions/entities/promotion-picture.entity.ts +++ b/src/modules/promotions/entities/promotion-picture.entity.ts @@ -1,15 +1,11 @@ -import type { PromotionPictureEntity } from '#types/api'; - import { Entity, OneToOne, Property } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; import { File } from '@modules/files/entities/file.entity'; import { Promotion } from './promotion.entity'; @Entity() -export class PromotionPicture extends File implements PromotionPictureEntity { - @ApiProperty({ type: Number, minimum: 1 }) +export class PromotionPicture extends File { @OneToOne(() => Promotion, (promotion) => promotion.picture, { nullable: true, owner: true }) picture_promotion: Promotion; diff --git a/src/modules/promotions/entities/promotion.entity.ts b/src/modules/promotions/entities/promotion.entity.ts index ec025aeb..f66cad71 100644 --- a/src/modules/promotions/entities/promotion.entity.ts +++ b/src/modules/promotions/entities/promotion.entity.ts @@ -1,7 +1,4 @@ -import type { PromotionEntity } from '#types/api'; - import { Cascade, Collection, Entity, OneToMany, OneToOne, Property } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; import { BaseEntity } from '@modules/_mixin/entities/base.entity'; import { User } from '@modules/users/entities/user.entity'; @@ -9,16 +6,13 @@ import { User } from '@modules/users/entities/user.entity'; import { PromotionPicture } from './promotion-picture.entity'; @Entity({ tableName: 'promotions' }) -export class Promotion extends BaseEntity implements PromotionEntity { +export class Promotion extends BaseEntity implements Promotion { @Property() - @ApiProperty({ type: Number, minimum: 1 }) number: number; @OneToMany(() => User, (user) => user.promotion, { cascade: [Cascade.REMOVE] }) - @ApiProperty({ type: Number }) users: Collection; @OneToOne(() => PromotionPicture, (picture) => picture.picture_promotion, { cascade: [Cascade.ALL], nullable: true }) - @ApiProperty({ type: PromotionPicture }) picture?: PromotionPicture; } diff --git a/src/modules/promotions/promotions.controller.ts b/src/modules/promotions/promotions.controller.ts index 0f3888aa..3374d4f6 100644 --- a/src/modules/promotions/promotions.controller.ts +++ b/src/modules/promotions/promotions.controller.ts @@ -26,13 +26,15 @@ import { } from '@nestjs/swagger'; import { z } from 'zod'; +import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; +import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { FilesService } from '@modules/files/files.service'; import { TranslateService } from '@modules/translate/translate.service'; import { validate } from '@utils/validate'; -import { PromotionResponseDTO } from './dto/promotion.dto'; +import { PromotionResponseDTO } from './dto/get.dto'; import { Promotion } from './entities/promotion.entity'; import { PromotionsService } from './promotions.service'; import { BaseUserResponseDTO } from '../users/dto/base-user.dto'; @@ -63,7 +65,7 @@ export class PromotionsController { @GuardPermissions('CAN_READ_PROMOTION') @ApiOkResponse({ type: PromotionResponseDTO }) @ApiOperation({ summary: 'Get the latest promotion' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) async getLatest() { return this.promotionsService.findLatest(); } @@ -71,9 +73,9 @@ export class PromotionsController { @Get('current') @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_PROMOTION') - @ApiOkResponse({ type: [PromotionResponseDTO] }) @ApiOperation({ summary: 'Get promotions currently active' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiOkResponse({ type: [PromotionResponseDTO] }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) async getCurrent() { return this.promotionsService.findCurrent(); } @@ -84,10 +86,10 @@ export class PromotionsController { @ApiConsumes('multipart/form-data') @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOperation({ summary: 'Update the promotion logo' }) - @ApiNotFoundResponse({ description: 'Promotion not found' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiBadRequestResponse({ description: 'Invalid file' }) - @ApiOkResponse({ type: Promotion }) + @ApiNotFoundResponse({ description: 'Promotion not found', type: ErrorResponseDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiBadRequestResponse({ description: 'Invalid file', type: ErrorResponseDTO }) + @ApiOkResponse({ type: PromotionResponseDTO }) @ApiBody({ schema: { type: 'object', @@ -112,9 +114,8 @@ export class PromotionsController { @GuardPermissions('CAN_READ_PROMOTION') @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOperation({ summary: 'Get the promotion logo' }) - @ApiNotFoundResponse({ description: 'Promotion not found' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiNotFoundResponse({ description: 'Promotion not found or promotion has no logo' }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiNotFoundResponse({ description: 'Promotion not found or promotion has no logo', type: ErrorResponseDTO }) async getLogo(@Param('number') number: number) { validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); @@ -127,9 +128,9 @@ export class PromotionsController { @GuardPermissions('CAN_EDIT_PROMOTION') @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOperation({ summary: 'Delete the promotion logo' }) - @ApiNotFoundResponse({ description: 'Promotion not found' }) - @ApiOkResponse({ type: Promotion }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiOkResponse({ type: MessageResponseDTO }) + @ApiNotFoundResponse({ description: 'Promotion not found', type: ErrorResponseDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) async deleteLogo(@Param('number') number: number) { validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); @@ -139,11 +140,11 @@ export class PromotionsController { @Get(':number') @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_PROMOTION') - @ApiOkResponse({ type: PromotionResponseDTO }) - @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOperation({ summary: 'Get the specified promotion' }) - @ApiNotFoundResponse({ description: 'Promotion not found' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) + @ApiOkResponse({ type: PromotionResponseDTO }) + @ApiNotFoundResponse({ description: 'Promotion not found', type: ErrorResponseDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) async get(@Param('number') number: number) { validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); @@ -153,11 +154,11 @@ export class PromotionsController { @Get(':number/users') @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_PROMOTION') - @ApiOkResponse({ type: [BaseUserResponseDTO] }) @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOperation({ summary: 'Get users of the specified promotions' }) - @ApiNotFoundResponse({ description: 'Promotion not found' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiOkResponse({ type: [BaseUserResponseDTO] }) + @ApiNotFoundResponse({ description: 'Promotion not found', type: ErrorResponseDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) async getUsers(@Param('number') number: number) { validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); diff --git a/src/modules/promotions/promotions.service.ts b/src/modules/promotions/promotions.service.ts index 472ddd9f..de9bef6e 100644 --- a/src/modules/promotions/promotions.service.ts +++ b/src/modules/promotions/promotions.service.ts @@ -5,10 +5,11 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { env } from '@env'; +import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; import { FilesService } from '@modules/files/files.service'; import { TranslateService } from '@modules/translate/translate.service'; -import { PromotionResponseDTO } from './dto/promotion.dto'; +import { PromotionPictureResponseDTO, PromotionResponseDTO } from './dto/get.dto'; import { PromotionPicture } from './entities/promotion-picture.entity'; import { Promotion } from './entities/promotion.entity'; import { BaseUserResponseDTO } from '../users/dto/base-user.dto'; @@ -39,7 +40,7 @@ export class PromotionsService { const res: PromotionResponseDTO[] = []; for (const promotion of promotions) { - res.push({ ...promotion, users: promotion.users.count() }); + res.push({ ...promotion, users: promotion.users.count(), picture: promotion.picture?.id }); } return res; @@ -54,6 +55,7 @@ export class PromotionsService { return { ...promotion, users: promotion.users.count(), + picture: promotion.picture?.id, }; } @@ -67,7 +69,7 @@ export class PromotionsService { const res: PromotionResponseDTO[] = []; for (const promotion of promotions) { - res.push({ ...promotion, users: promotion.users.count() }); + res.push({ ...promotion, users: promotion.users.count(), picture: promotion.picture?.id }); } return res; @@ -81,6 +83,7 @@ export class PromotionsService { return { ...promotion, users: promotion.users.count(), + picture: promotion.picture?.id, }; } @@ -106,7 +109,7 @@ export class PromotionsService { } @CreateRequestContext() - async updateLogo(number: number, file: Express.Multer.File): Promise { + async updateLogo(number: number, file: Express.Multer.File): Promise { const promotion = await this.orm.em.findOne(Promotion, { number }, { populate: ['picture'] }); if (!promotion) throw new NotFoundException(this.t.Errors.Id.NotFound(Promotion, number)); @@ -137,11 +140,7 @@ export class PromotionsService { await this.orm.em.persistAndFlush(promotion); - // Fix issue with the picture not being populated - // -> happens when the picture is updated - const out = await this.orm.em.findOne(Promotion, { number }, { fields: ['*'], populate: ['picture'] }); - delete out.picture.picture_promotion; // avoid circular reference - return out; + return { ...promotion.picture, picture_promotion: promotion.number, visibility: promotion.picture.visibility?.id }; } @CreateRequestContext() @@ -156,7 +155,7 @@ export class PromotionsService { } @CreateRequestContext() - async deleteLogo(number: number): Promise { + async deleteLogo(number: number): Promise { const promotion = await this.orm.em.findOne(Promotion, { number }, { populate: ['picture'] }); if (!promotion) throw new NotFoundException(this.t.Errors.Id.NotFound(Promotion, number)); @@ -165,6 +164,6 @@ export class PromotionsService { this.filesService.deleteFromDisk(promotion.picture); await this.orm.em.removeAndFlush(promotion.picture); - return promotion; + return { message: this.t.Success.Entity.Deleted(PromotionPicture), statusCode: 200 }; } } diff --git a/src/modules/roles/dto/get.dto.ts b/src/modules/roles/dto/get.dto.ts new file mode 100644 index 00000000..b1d23634 --- /dev/null +++ b/src/modules/roles/dto/get.dto.ts @@ -0,0 +1,31 @@ +import type { IRoleGetDTO, IRoleUsersResponseDTO, PERMISSION_NAMES } from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsBoolean, IsDate, IsInt, IsString } from 'class-validator'; + +import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; +import { BaseUserResponseDTO } from '@modules/users/dto/base-user.dto'; + +export class RoleUsersResponseDTO extends BaseUserResponseDTO implements IRoleUsersResponseDTO { + @ApiProperty({ type: Date }) + @IsDate() + role_expires: Date; +} + +export class RoleGetDTO implements IRoleGetDTO { + @ApiProperty({ type: String, example: 'AE_ADMIN' }) + @IsString() // TODO: Add custom validator to check if it's uppercase + name: Uppercase; + + @ApiProperty({ type: Boolean, default: false }) + @IsBoolean() + revoked: boolean; + + @ApiProperty({ enum: PERMISSIONS_NAMES, isArray: true }) + @IsArray() // TODO: Add custom validator to check if it's array of uppercase strings + permissions: PERMISSION_NAMES[]; + + @ApiProperty({ type: Number, default: 1 }) + @IsInt() + users: number; +} diff --git a/src/modules/roles/dto/patch.dto.ts b/src/modules/roles/dto/patch.dto.ts index 5850f085..c16ec503 100644 --- a/src/modules/roles/dto/patch.dto.ts +++ b/src/modules/roles/dto/patch.dto.ts @@ -1,19 +1,19 @@ -import type { RolePatchDto, RoleEditUserDto } from '#types/api'; +import type { IRolePatchDTO, IRoleEditUserDTO } from '#types/api'; import { ApiProperty } from '@nestjs/swagger'; -import { IsDate, IsNumber } from 'class-validator'; +import { IsDate, IsInt } from 'class-validator'; import { RolePostDTO } from './post.dto'; -export class RolePatchDTO extends RolePostDTO implements RolePatchDto { +export class RolePatchDTO extends RolePostDTO implements IRolePatchDTO { @ApiProperty({ required: true, minimum: 1 }) - @IsNumber() + @IsInt() id: number; } -export class RoleEditUserDTO implements RoleEditUserDto { +export class RoleEditUserDTO implements IRoleEditUserDTO { @ApiProperty({ required: true, type: Number, minimum: 1 }) - @IsNumber() + @IsInt() id: number; @ApiProperty({ required: true, type: Date }) diff --git a/src/modules/roles/dto/post.dto.ts b/src/modules/roles/dto/post.dto.ts index aa837702..66dd460c 100644 --- a/src/modules/roles/dto/post.dto.ts +++ b/src/modules/roles/dto/post.dto.ts @@ -1,11 +1,11 @@ -import type { RolePostDto, PERMISSION_NAMES } from '#types/api'; +import type { IRolePostDTO, PERMISSION_NAMES } from '#types/api'; import { ApiProperty } from '@nestjs/swagger'; import { IsString } from 'class-validator'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -export class RolePostDTO implements RolePostDto { +export class RolePostDTO implements IRolePostDTO { @ApiProperty({ type: String, example: 'AE_ADMINS' }) @IsString() name: Uppercase; diff --git a/src/modules/roles/dto/users.dto.ts b/src/modules/roles/dto/users.dto.ts deleted file mode 100644 index d6ae5c6d..00000000 --- a/src/modules/roles/dto/users.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { RoleUsersResponseDto } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; - -import { BaseUserResponseDTO } from '@modules/users/dto/base-user.dto'; - -export class RoleUsersResponseDTO extends BaseUserResponseDTO implements RoleUsersResponseDto { - @ApiProperty({ type: Date }) - role_expires: Date; -} diff --git a/src/modules/roles/entities/role-expiration.entity.ts b/src/modules/roles/entities/role-expiration.entity.ts index bb328dc1..4bf23feb 100644 --- a/src/modules/roles/entities/role-expiration.entity.ts +++ b/src/modules/roles/entities/role-expiration.entity.ts @@ -8,15 +8,12 @@ import { Role } from './role.entity'; @Entity({ tableName: 'roles_expirations' }) export class RoleExpiration extends BaseEntity { - /** Specify which user is attached to that role */ @ManyToOne(() => User) user: User; - /** Specify which role is attached to that user */ @ManyToOne(() => Role) role: Role; - /** Specify when the role should expires */ @Property({ name: 'expires_at' }) @ApiProperty() expires: Date; diff --git a/src/modules/roles/entities/role.entity.ts b/src/modules/roles/entities/role.entity.ts index ec473f04..65c12d39 100644 --- a/src/modules/roles/entities/role.entity.ts +++ b/src/modules/roles/entities/role.entity.ts @@ -1,9 +1,7 @@ -import type { PERMISSION_NAMES, RoleEntity } from '#types/api'; +import type { PERMISSION_NAMES } from '#types/api'; import { Collection, Entity, ManyToMany, Property } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; -import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; import { BaseEntity } from '@modules/_mixin/entities/base.entity'; import { User } from '@modules/users/entities/user.entity'; @@ -11,24 +9,16 @@ import { User } from '@modules/users/entities/user.entity'; * Entity used to store roles, which are a collection of permissions */ @Entity({ tableName: 'roles' }) -export class Role extends BaseEntity implements RoleEntity { - /** Name of the role, in caps */ +export class Role extends BaseEntity { @Property({ unique: true }) - @ApiProperty({ type: String, example: 'AE_ADMIN' }) name: Uppercase; - /** Determine wether the role is still active */ @Property({ name: 'is_revoked', onCreate: () => false }) - @ApiProperty({ type: Boolean, default: false }) revoked: boolean; - /** Specify what permissions the role has */ @Property({ name: 'permissions' }) - @ApiProperty({ enum: PERMISSIONS_NAMES, isArray: true }) permissions: PERMISSION_NAMES[]; - /** Specify to which user the role is attached */ @ManyToMany(() => User, (user) => user.roles, { owner: true }) - @ApiProperty({ type: Number, default: 1 }) users = new Collection(this); } diff --git a/src/modules/roles/roles.controller.ts b/src/modules/roles/roles.controller.ts index b9589cd5..9aede43a 100644 --- a/src/modules/roles/roles.controller.ts +++ b/src/modules/roles/roles.controller.ts @@ -12,14 +12,15 @@ import { } from '@nestjs/swagger'; import { z } from 'zod'; +import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { TranslateService } from '@modules/translate/translate.service'; import { validate } from '@utils/validate'; +import { RoleGetDTO, RoleUsersResponseDTO } from './dto/get.dto'; import { RoleEditUserDTO, RolePatchDTO } from './dto/patch.dto'; import { RolePostDTO } from './dto/post.dto'; -import { RoleUsersResponseDTO } from './dto/users.dto'; import { Role } from './entities/role.entity'; import { RolesService } from './roles.service'; @@ -34,9 +35,9 @@ export class RolesController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_ROLE') @ApiOperation({ summary: 'Create a new role' }) - @ApiOkResponse({ type: Role }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiBadRequestResponse({ description: 'Role name is not uppercase or already exists' }) + @ApiOkResponse({ type: RoleGetDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiBadRequestResponse({ description: 'Role name is not uppercase or already exists', type: ErrorResponseDTO }) async createRole(@Body() body: RolePostDTO) { const schema = z .object({ @@ -53,10 +54,10 @@ export class RolesController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_ROLE') @ApiOperation({ summary: 'Update an existing role' }) - @ApiOkResponse({ type: Role }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiBadRequestResponse({ description: 'Role name is not uppercase' }) - @ApiNotFoundResponse({ description: 'Role not found' }) + @ApiOkResponse({ type: RoleGetDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiBadRequestResponse({ description: 'Role name is not uppercase', type: ErrorResponseDTO }) + @ApiNotFoundResponse({ description: 'Role not found', type: ErrorResponseDTO }) async editRole(@Body() body: RolePatchDTO) { const schema = z .object({ @@ -77,8 +78,8 @@ export class RolesController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_ROLE') @ApiOperation({ summary: 'Get all existing roles' }) - @ApiOkResponse({ type: [Role] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiOkResponse({ type: [RoleGetDTO] }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) async getAllRoles() { return this.rolesService.getAllRoles(); } @@ -87,9 +88,9 @@ export class RolesController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_ROLE') @ApiOperation({ summary: 'Get the specified role' }) - @ApiOkResponse({ type: Role }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiNotFoundResponse({ description: 'Role not found' }) + @ApiOkResponse({ type: RoleGetDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiNotFoundResponse({ description: 'Role not found', type: ErrorResponseDTO }) async getRole(@Param('role_id') id: number) { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(Role, id)); @@ -101,8 +102,8 @@ export class RolesController { @GuardPermissions('CAN_READ_ROLE') @ApiOperation({ summary: 'Get user(s) of the specified role' }) @ApiOkResponse({ type: [RoleUsersResponseDTO] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiNotFoundResponse({ description: 'Role not found' }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiNotFoundResponse({ description: 'Role not found', type: ErrorResponseDTO }) async getRoleUsers(@Param('role_id') id: number) { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(Role, id)); @@ -114,8 +115,8 @@ export class RolesController { @GuardPermissions('CAN_EDIT_ROLE') @ApiOperation({ summary: 'Add user(s) to the role' }) @ApiOkResponse({ type: [RoleUsersResponseDTO] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiNotFoundResponse({ description: 'Role not found' }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiNotFoundResponse({ description: 'Role not found', type: ErrorResponseDTO }) @ApiBody({ type: [RoleEditUserDTO] }) async addUsersToRole(@Param('role_id') role_id: number, @Body() body: RoleEditUserDTO[]) { validate(z.coerce.number().int().min(1), role_id, this.t.Errors.Id.Invalid(Role, role_id)); @@ -136,8 +137,8 @@ export class RolesController { @GuardPermissions('CAN_EDIT_ROLE') @ApiOperation({ summary: 'Remove user(s) from the role' }) @ApiOkResponse({ type: [RoleUsersResponseDTO] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiNotFoundResponse({ description: 'Role not found' }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiNotFoundResponse({ description: 'Role not found', type: ErrorResponseDTO }) @ApiBody({ type: [Number] }) async removeUsersToRole(@Param('role_id') role_id: number, @Body('') body: number[]) { validate(z.coerce.number().int().min(1), role_id, this.t.Errors.Id.Invalid(Role, role_id)); diff --git a/src/modules/roles/roles.service.ts b/src/modules/roles/roles.service.ts index 6e716622..2a958c01 100644 --- a/src/modules/roles/roles.service.ts +++ b/src/modules/roles/roles.service.ts @@ -10,8 +10,8 @@ import { BaseUserResponseDTO } from '@modules/users/dto/base-user.dto'; import { User } from '@modules/users/entities/user.entity'; import { UsersDataService } from '@modules/users/services/users-data.service'; +import { RoleGetDTO, RoleUsersResponseDTO } from './dto/get.dto'; import { RolePatchDTO } from './dto/patch.dto'; -import { RoleUsersResponseDTO } from './dto/users.dto'; import { RoleExpiration } from './entities/role-expiration.entity'; import { Role } from './entities/role.entity'; @@ -47,13 +47,13 @@ export class RolesService { * @returns the array of all roles */ @CreateRequestContext() - async getAllRoles(): Promise<(Omit & { users: number })[]> { + async getAllRoles(): Promise { const roles = await this.orm.em.find(Role, {}, { populate: ['users'] }); return roles.map((r) => ({ ...r, users: r.users.count() })); } @CreateRequestContext() - async getRole(id: number): Promise & { users: number }> { + async getRole(id: number): Promise { const role = await this.orm.em.findOne(Role, { id }, { populate: ['users'] }); if (!role) throw new NotFoundException(this.t.Errors.Id.NotFound(Role, id)); @@ -61,7 +61,7 @@ export class RolesService { } @CreateRequestContext() - async createRole(name: string, permissions: PERMISSION_NAMES[]): Promise> { + async createRole(name: string, permissions: PERMISSION_NAMES[]): Promise { const roleName = name.toUpperCase(); if (await this.orm.em.findOne(Role, { name: roleName })) @@ -78,11 +78,11 @@ export class RolesService { await this.orm.em.persistAndFlush(role); delete role.users; - return role; + return { ...role, users: 0 }; } @CreateRequestContext() - async editRole(input: RolePatchDTO): Promise & { users: number }> { + async editRole(input: RolePatchDTO): Promise { const role = await this.orm.em.findOne(Role, { id: input.id }, { populate: ['users'] }); if (!role) throw new NotFoundException(this.t.Errors.Id.NotFound(Role, input.id)); diff --git a/src/modules/subscription/entities/subscription.entity.ts b/src/modules/subscription/entities/subscription.entity.ts deleted file mode 100644 index 1b24d0ad..00000000 --- a/src/modules/subscription/entities/subscription.entity.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { SubscriptionEntity } from '#types/api'; - -import { Entity, ManyToOne, Property } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; - -import { BaseEntity } from '@modules/_mixin/entities/base.entity'; -import { User } from '@modules/users/entities/user.entity'; - -@Entity({ tableName: 'subscriptions' }) -export class Subscription extends BaseEntity implements SubscriptionEntity { - @ManyToOne(() => User, { onDelete: 'cascade', joinColumn: 'user_id' }) - @ApiProperty({ type: Number }) - user: User; - - @Property() - @ApiProperty() - plan: string; - - @Property({ type: 'date' }) - @ApiProperty() - expires: Date; - - /** Determine if the subscription is still active by comparing current date with the subscription date */ - @Property({ persist: false }) - get is_active(): boolean { - return this.expires > new Date(); - } -} diff --git a/src/modules/users/controllers/users-data.controller.ts b/src/modules/users/controllers/users-data.controller.ts index d7faa6a7..f636b654 100644 --- a/src/modules/users/controllers/users-data.controller.ts +++ b/src/modules/users/controllers/users-data.controller.ts @@ -6,23 +6,24 @@ import { ApiBearerAuth, ApiBody, ApiOkResponse, ApiOperation, ApiTags, ApiUnauth import { z } from 'zod'; import { USER_GENDER } from '@exported/api/constants/genders'; -import { MessageResponseDTO } from '@modules/_mixin/dto/message-response.dto'; +import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; +import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { GuardSelfOrPermsOrSub } from '@modules/auth/decorators/self-or-sub-perms.decorator'; import { GuardSelfParam } from '@modules/auth/decorators/self.decorator'; -import { UserPostByAdminDTO } from '@modules/auth/dto/register.dto'; +import { CreateUserDTO } from '@modules/auth/dto/post.dto'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { SelfOrPermissionGuard } from '@modules/auth/guards/self-or-perms.guard'; import { SelfOrPermsOrSubGuard } from '@modules/auth/guards/self-or-sub-or-perms.guard'; import { SelfGuard } from '@modules/auth/guards/self.guard'; -import { Permission } from '@modules/permissions/entities/permission.entity'; +import { PermissionGetDTO } from '@modules/permissions/dto/get.dto'; import { TranslateService } from '@modules/translate/translate.service'; import { validate } from '@utils/validate'; -import { UserRolesGetDTO } from '../dto/get.dto'; +import { BaseUserResponseDTO } from '../dto/base-user.dto'; +import { UserGetDTO, UserRoleGetDTO, UserVisibilityGetDTO } from '../dto/get.dto'; import { UserPatchDTO, UserVisibilityPatchDTO } from '../dto/patch.dto'; -import { UserVisibility } from '../entities/user-visibility.entity'; import { User } from '../entities/user.entity'; import { UsersDataService } from '../services/users-data.service'; @@ -37,10 +38,10 @@ export class UsersDataController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_USER') @ApiOperation({ summary: 'Creates new users' }) - @ApiOkResponse({ description: 'The created user', type: [User] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiBody({ type: [UserPostByAdminDTO] }) - async create(@Body() input: UserPostByAdminDTO[]) { + @ApiOkResponse({ description: 'The created user', type: [BaseUserResponseDTO] }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiBody({ type: [CreateUserDTO] }) + async create(@Body() input: CreateUserDTO[]): Promise { const schema = z .object({ email: z.string().email(), @@ -59,10 +60,10 @@ export class UsersDataController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Update users data' }) - @ApiOkResponse({ description: 'The updated users', type: User }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiOkResponse({ description: 'The updated users', type: UserGetDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) @ApiBody({ type: [UserPatchDTO] }) - async update(@Req() req: RequestWithUser, @Body() input: UserPatchDTO[]) { + async update(@Req() req: RequestWithUser, @Body() input: UserPatchDTO[]): Promise { const schema = z .object({ id: z.coerce.number(), @@ -85,7 +86,7 @@ export class UsersDataController { .strict(); validate(z.array(schema).min(1), input); - return this.usersService.update(req.user.id, input); + return this.usersService.update((req.user as User).id, input); } @Delete(':id') @@ -93,8 +94,8 @@ export class UsersDataController { @GuardSelfParam('id') @ApiOperation({ summary: 'Delete your account' }) @ApiOkResponse({ description: 'User deleted', type: MessageResponseDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - async delete(@Param('id') id: number) { + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + async delete(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); return this.usersService.delete(id); @@ -104,33 +105,33 @@ export class UsersDataController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_READ_USER_PRIVATE']) @ApiOperation({ summary: 'Get all information of a user' }) - @ApiOkResponse({ description: 'User data', type: User }) + @ApiOkResponse({ description: 'User data', type: UserGetDTO }) @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - async getPrivate(@Param('id') id: number) { + async getPrivate(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - return this.usersService.findOne(id, false); + return this.usersService.findOneAsDTO(id, false); } @Get(':id/data/public') @UseGuards(SelfOrPermsOrSubGuard) @GuardSelfOrPermsOrSub('id', ['CAN_READ_USER']) @ApiOperation({ summary: 'Get publicly available information of a user' }) - @ApiOkResponse({ description: 'User data, excepted privates fields (set in the visibility table)', type: User }) + @ApiOkResponse({ description: 'User data, excepted privates fields (set in the visibility table)', type: UserGetDTO }) @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - async getPublic(@Param('id') id: number) { + async getPublic(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - return this.usersService.findOne(id); + return this.usersService.findOneAsDTO(id); } @Get(':id/data/visibility') @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_READ_USER_PRIVATE']) @ApiOperation({ summary: 'Get visibility settings of a user' }) - @ApiOkResponse({ description: 'User data', type: UserVisibility }) + @ApiOkResponse({ description: 'User data', type: UserVisibilityGetDTO }) @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - async getVisibility(@Param('id') id: number) { + async getVisibility(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); return (await this.usersService.findVisibilities(id))[0]; @@ -140,9 +141,12 @@ export class UsersDataController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Update visibility settings of a user' }) - @ApiOkResponse({ description: 'User data', type: UserVisibility }) + @ApiOkResponse({ description: 'User data', type: UserVisibilityGetDTO }) @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - async updateVisibility(@Param('id') id: number, @Body() input: UserVisibilityPatchDTO) { + async updateVisibility( + @Param('id') id: number, + @Body() input: UserVisibilityPatchDTO, + ): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); const schema = z @@ -166,9 +170,9 @@ export class UsersDataController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_READ_USER', 'CAN_READ_ROLE']) @ApiOperation({ summary: 'Get roles of a user' }) - @ApiOkResponse({ description: 'Roles of the user', type: [UserRolesGetDTO] }) + @ApiOkResponse({ description: 'Roles of the user', type: [UserRoleGetDTO] }) @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - async getUserRoles(@Param('id') id: number) { + async getUserRoles(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); return this.usersService.getUserRoles(id, { show_expired: true, show_revoked: true }); @@ -178,9 +182,9 @@ export class UsersDataController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_READ_USER', 'CAN_READ_PERMISSIONS_OF_USER']) @ApiOperation({ summary: 'Get permissions of a user' }) - @ApiOkResponse({ description: 'Permissions of the user', type: [Permission] }) + @ApiOkResponse({ description: 'Permissions of the user', type: [PermissionGetDTO] }) @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - async getUserPermissions(@Param('id') id: number) { + async getUserPermissions(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); return this.usersService.getUserPermissions(id, { show_expired: true, show_revoked: true }); diff --git a/src/modules/users/controllers/users-files.controller.ts b/src/modules/users/controllers/users-files.controller.ts index e411b366..919344e8 100644 --- a/src/modules/users/controllers/users-files.controller.ts +++ b/src/modules/users/controllers/users-files.controller.ts @@ -1,5 +1,3 @@ -import type { RequestWithUser } from '#types/api'; - import { BadRequestException, Controller, @@ -27,6 +25,8 @@ import { } from '@nestjs/swagger'; import { z } from 'zod'; +import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; +import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { GuardSelfOrPermsOrSub } from '@modules/auth/decorators/self-or-sub-perms.decorator'; @@ -37,9 +37,8 @@ import { FilesService } from '@modules/files/files.service'; import { TranslateService } from '@modules/translate/translate.service'; import { validate } from '@utils/validate'; -import { UserBanner } from '../entities/user-banner.entity'; -import { UserPicture } from '../entities/user-picture.entity'; -import { User } from '../entities/user.entity'; +import { UserGetBannerDTO, UserGetPictureDTO } from '../dto/get.dto'; +import { RequestWithUser, User } from '../entities/user.entity'; import { UsersFilesService } from '../services/users-files.service'; @ApiTags('Users Files') @@ -57,8 +56,8 @@ export class UsersFilesController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Update user profile picture' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiOkResponse({ description: 'The updated user picture', type: UserPicture }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiOkResponse({ description: 'The updated user picture', type: UserGetPictureDTO }) @ApiConsumes('multipart/form-data') @ApiBody({ schema: { @@ -72,20 +71,24 @@ export class UsersFilesController { }, }) @UseInterceptors(FileInterceptor('file')) - async editPicture(@Req() req: RequestWithUser, @UploadedFile() file: Express.Multer.File, @Param('id') id: number) { + async editPicture( + @Req() req: RequestWithUser, + @UploadedFile() file: Express.Multer.File, + @Param('id') id: number, + ): Promise { if (!file) throw new BadRequestException(this.t.Errors.File.NotProvided()); validate(z.coerce.number().int().min(1), id); - return this.usersFilesService.updatePicture(req.user as User, id, file); + return this.usersFilesService.updatePicture(req.user, id, file); } @Delete(':id/picture') @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_USER') @ApiOperation({ summary: 'Delete user profile picture' }) - @ApiOkResponse({ type: User }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - async deletePicture(@Param('id') id: number) { + @ApiOkResponse({ type: MessageResponseDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + async deletePicture(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); return this.usersFilesService.deletePicture(id); @@ -95,14 +98,15 @@ export class UsersFilesController { @UseGuards(SelfOrPermsOrSubGuard) @GuardSelfOrPermsOrSub('id', ['CAN_READ_USER']) @ApiOperation({ summary: 'Get user profile picture' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiOkResponse({ description: 'The user picture', type: ArrayBuffer }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) async getPicture(@Req() req: RequestWithUser, @Param('id') id: number) { validate(z.coerce.number().int().min(1), id); const picture = await this.usersFilesService.getPicture(id); await picture.visibility?.init(); - if (await this.filesService.canReadFile(picture, req.user as User)) + if (await this.filesService.canReadFile(picture, req.user)) return new StreamableFile(this.filesService.toReadable(picture)); // Should not happen unless the user is subscribed but not in the visibility group of subscribers @@ -115,8 +119,8 @@ export class UsersFilesController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Update user profile banner' }) - @ApiOkResponse({ description: 'The updated user banner', type: UserBanner }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiOkResponse({ description: 'The updated user banner', type: UserGetBannerDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) @ApiConsumes('multipart/form-data') @ApiBody({ schema: { @@ -130,7 +134,7 @@ export class UsersFilesController { }, }) @UseInterceptors(FileInterceptor('file')) - async editBanner(@UploadedFile() file: Express.Multer.File, @Param('id') id: number) { + async editBanner(@UploadedFile() file: Express.Multer.File, @Param('id') id: number): Promise { if (!file) throw new BadRequestException(this.t.Errors.File.NotProvided()); validate(z.coerce.number().int().min(1), id); @@ -141,9 +145,9 @@ export class UsersFilesController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Delete user profile banner' }) - @ApiOkResponse({ type: User }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - async deleteBanner(@Param('id') id: number) { + @ApiOkResponse({ type: MessageResponseDTO }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + async deleteBanner(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); return this.usersFilesService.deleteBanner(id); @@ -153,14 +157,15 @@ export class UsersFilesController { @UseGuards(SelfOrPermsOrSubGuard) @GuardSelfOrPermsOrSub('id', ['CAN_READ_USER']) @ApiOperation({ summary: 'Get user profile banner' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiOkResponse({ description: 'The user banner', type: ArrayBuffer }) async getBanner(@Req() req: RequestWithUser, @Param('id') id: number) { validate(z.coerce.number().int().min(1), id); const banner = await this.usersFilesService.getBanner(id); await banner.visibility?.init(); - if (await this.filesService.canReadFile(banner, req.user as User)) + if (await this.filesService.canReadFile(banner, req.user)) return new StreamableFile(this.filesService.toReadable(banner)); // Should not happen unless the user is subscribed but not in the visibility group of subscribers diff --git a/src/modules/users/dto/base-user.dto.ts b/src/modules/users/dto/base-user.dto.ts index 1dfd9eda..bce3e703 100644 --- a/src/modules/users/dto/base-user.dto.ts +++ b/src/modules/users/dto/base-user.dto.ts @@ -1,18 +1,16 @@ +import type { IBaseUserDTO } from '#types/api'; + import { ApiProperty } from '@nestjs/swagger'; -import { BaseResponseDTO } from '@modules/_mixin/dto/base-response.dto'; -import { User } from '@modules/users/entities/user.entity'; +import { BaseResponseDTO } from '@modules/_mixin/dto/base.dto'; -export class BaseUserResponseDTO - extends BaseResponseDTO - implements Pick -{ +export class BaseUserResponseDTO extends BaseResponseDTO implements IBaseUserDTO { @ApiProperty() first_name: string; @ApiProperty() last_name: string; - @ApiProperty() + @ApiProperty({ required: false }) nickname?: string; } diff --git a/src/modules/users/dto/get.dto.ts b/src/modules/users/dto/get.dto.ts index c92e2851..aac9d5a5 100644 --- a/src/modules/users/dto/get.dto.ts +++ b/src/modules/users/dto/get.dto.ts @@ -1,12 +1,23 @@ +import type { email } from '#types'; +import type { + IUserBannerResponseDTO, + IUserPictureResponseDTO, + IUserRoleGetDTO, + IUserVisibilityGetDTO, +} from '#types/api'; + import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsDate, IsNumber, IsString } from 'class-validator'; +import { IsBoolean, IsDate, IsEmail, IsIn, IsInt, IsNumber, IsString } from 'class-validator'; -import { PERMISSION_NAMES, type UserRolesGetDto } from '#types/api'; +import { IUserGetDTO, PERMISSION_NAMES } from '#types/api'; +import { USER_GENDER } from '@exported/api/constants/genders'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; +import { BaseResponseDTO } from '@modules/_mixin/dto/base.dto'; +import { FileGetDTO } from '@modules/files/dto/get.dto'; -export class UserRolesGetDTO implements UserRolesGetDto { +export class UserRoleGetDTO implements IUserRoleGetDTO { @ApiProperty({ required: true, minimum: 1 }) - @IsNumber() + @IsInt() id: number; @ApiProperty({ required: true, type: Date }) @@ -33,3 +44,124 @@ export class UserRolesGetDTO implements UserRolesGetDto { @IsString() permissions: Array; } + +export class UserGetDTO extends BaseResponseDTO implements IUserGetDTO { + @ApiProperty({ example: 'John' }) + @IsString() + first_name: string; + + @ApiProperty({ example: 'Doe' }) + @IsString() + last_name: string; + + @ApiProperty({ minimum: 1 }) + @IsNumber() + picture?: number; + + @ApiProperty({ minimum: 1 }) + @IsNumber() + banner?: number; + + @ApiProperty({ type: String, example: 'example@domain.com' }) + @IsEmail() + email: email; + + @ApiProperty({ type: Boolean, default: false }) + @IsBoolean() + email_verified: boolean; + + @ApiProperty({ example: new Date('1999-12-31').toISOString() }) + @IsDate() + birth_date: Date; + + @ApiProperty() + @IsString() + nickname?: string; + + @ApiProperty({ example: USER_GENDER[0], enum: USER_GENDER }) + @IsString() + @IsIn(USER_GENDER) + gender?: (typeof USER_GENDER)[number]; + + @ApiProperty({ example: null }) + @IsString() + pronouns?: string; + + @ApiProperty({ type: Number, minimum: 1 }) + @IsNumber() + promotion?: number; + + @ApiProperty({ example: new Date().toISOString() }) + @IsDate() + last_seen?: Date; + + @ApiProperty({ example: false }) + @IsBoolean() + subscribed: boolean; // TODO: (KEY: 2) Make a PR to implement subscriptions in the API + + @ApiProperty() + @IsEmail() + secondary_email?: string; + + @ApiProperty() + @IsString() + phone?: string; + + @ApiProperty() + @IsString() + parent_contact?: string; + + @ApiProperty({ type: Date }) + @IsDate() + verified?: Date; +} + +export class UserVisibilityGetDTO implements IUserVisibilityGetDTO { + @ApiProperty({ minimum: 1 }) + @IsInt() + user: number; + + @ApiProperty({ type: Boolean, default: false }) + @IsBoolean() + email: boolean; + + @ApiProperty({ type: Boolean, default: false }) + @IsBoolean() + secondary_email: boolean; + + @ApiProperty({ type: Boolean, default: true }) + @IsBoolean() + birth_date: boolean; + + @ApiProperty({ type: Boolean, default: false }) + @IsBoolean() + gender: boolean; + + @ApiProperty({ type: Boolean, default: false }) + @IsBoolean() + pronouns: boolean; + + @ApiProperty({ type: Boolean, default: true }) + @IsBoolean() + promotion: boolean; + + @ApiProperty({ type: Boolean, default: false }) + @IsBoolean() + phone: boolean; + + @ApiProperty({ type: Boolean, default: false }) + @IsBoolean() + parent_contact: boolean; +} + +export class UserGetPictureDTO extends FileGetDTO implements IUserPictureResponseDTO { + @ApiProperty({ minimum: 1 }) + @IsInt() + picture_user: number; +} + +export class UserGetBannerDTO extends FileGetDTO implements IUserBannerResponseDTO { + @ApiProperty({ minimum: 1 }) + @IsInt() + banner_user: number; +} diff --git a/src/modules/users/dto/patch.dto.ts b/src/modules/users/dto/patch.dto.ts index f36bb24c..edc94918 100644 --- a/src/modules/users/dto/patch.dto.ts +++ b/src/modules/users/dto/patch.dto.ts @@ -1,102 +1,6 @@ -import type { email } from '#types'; -import type { UserVisibilityPatchDto, UserPatchDto } from '#types/api'; +import type { IUserVisibilityPatchDTO, IUserPatchDTO } from '#types/api'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsDate, IsEmail, IsNumber, IsString } from 'class-validator'; +import { UserGetDTO, UserVisibilityGetDTO } from './get.dto'; -export class UserPatchDTO implements UserPatchDto { - @ApiProperty({ required: true, minimum: 1 }) - @IsNumber() - id: number; - - @ApiProperty({ required: false }) - @IsEmail() - email?: email; - - @ApiProperty({ required: false }) - @IsString() - password?: string; - - @ApiProperty({ required: false }) - @IsDate() - birth_date?: Date; - - @ApiProperty({ required: false }) - @IsString() - first_name?: string; - - @ApiProperty({ required: false }) - @IsString() - last_name?: string; - - @ApiProperty({ required: false }) - @IsString() - nickname?: string; - - @ApiProperty({ required: false }) - @IsString() - gender?: string; - - @ApiProperty({ required: false }) - @IsString() - pronouns?: string; - - @ApiProperty({ required: false }) - @IsString() - secondary_email?: string; - - @ApiProperty({ required: false }) - @IsString() - phone?: string; - - @ApiProperty({ required: false }) - @IsString() - parent_contact?: string; - - // TODO: (KEY: 1) Make a PR to implement cursus & specialty in the API - // @ApiProperty({ required: false }) - // @IsString() - // cursus?: string; - - // @ApiProperty({ required: false }) - // @IsString() - // specialty?: string; - - @ApiProperty({ required: false, minimum: 1 }) - @IsNumber() - promotion?: number; -} - -export class UserVisibilityPatchDTO implements UserVisibilityPatchDto { - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - email: boolean; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - secondary_email: boolean; - - @ApiProperty({ type: Boolean, default: true }) - @IsBoolean() - birth_date: boolean; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - gender: boolean; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - pronouns: boolean; - - @ApiProperty({ type: Boolean, default: true }) - @IsBoolean() - promotion: boolean; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - phone: boolean; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - parent_contact: boolean; -} +export class UserPatchDTO extends UserGetDTO implements IUserPatchDTO {} +export class UserVisibilityPatchDTO extends UserVisibilityGetDTO implements IUserVisibilityPatchDTO {} diff --git a/src/modules/users/entities/user-banner.entity.ts b/src/modules/users/entities/user-banner.entity.ts index 0e853c3e..318f8928 100644 --- a/src/modules/users/entities/user-banner.entity.ts +++ b/src/modules/users/entities/user-banner.entity.ts @@ -1,15 +1,11 @@ -import type { UserBannerEntity } from '#types/api'; - import { Entity, OneToOne, Property } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; import { File } from '@modules/files/entities/file.entity'; import { User } from './user.entity'; @Entity() -export class UserBanner extends File implements UserBannerEntity { - @ApiProperty({ type: Number, minimum: 1 }) +export class UserBanner extends File { @OneToOne(() => User, (user) => user.banner, { nullable: true, owner: true }) banner_user: User; diff --git a/src/modules/users/entities/user-picture.entity.ts b/src/modules/users/entities/user-picture.entity.ts index 179f32c2..86349300 100644 --- a/src/modules/users/entities/user-picture.entity.ts +++ b/src/modules/users/entities/user-picture.entity.ts @@ -1,15 +1,11 @@ -import type { UserPictureEntity } from '#types/api'; - import { Entity, OneToOne, Property } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; import { File } from '@modules/files/entities/file.entity'; import { User } from './user.entity'; @Entity() -export class UserPicture extends File implements UserPictureEntity { - @ApiProperty({ type: Number, minimum: 1 }) +export class UserPicture extends File { @OneToOne(() => User, (user) => user.picture, { nullable: true, owner: true }) picture_user: User; diff --git a/src/modules/users/entities/user-visibility.entity.ts b/src/modules/users/entities/user-visibility.entity.ts index d2cb144f..4d6a4ebb 100644 --- a/src/modules/users/entities/user-visibility.entity.ts +++ b/src/modules/users/entities/user-visibility.entity.ts @@ -1,44 +1,36 @@ -import type { UserVisibilityEntity } from '#types/api'; - import { Entity, Property, OneToOne } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; import { BaseEntity } from '@modules/_mixin/entities/base.entity'; import { User } from './user.entity'; @Entity({ tableName: 'users_visibility' }) -export class UserVisibility extends BaseEntity implements UserVisibilityEntity { +export class UserVisibility extends BaseEntity { /** Specify to which user those parameters belongs */ @OneToOne(() => User, { onDelete: 'cascade', joinColumn: 'user_id' }) - @ApiProperty({ type: Number, minimum: 1 }) user: User; /** Wether the user email should be visible or not */ @Property({ onCreate: () => false }) - @ApiProperty({ type: Boolean, default: false }) email: boolean; /** Wether the user email should be visible or not */ @Property({ onCreate: () => false }) - @ApiProperty({ type: Boolean, default: false }) secondary_email: boolean; /** Wether the user birth_date should be visible or not */ @Property({ onCreate: () => true }) - @ApiProperty({ type: Boolean, default: true }) birth_date: boolean; /** Wether the user gender should be visible or not */ @Property({ onCreate: () => false }) - @ApiProperty({ type: Boolean, default: false }) gender: boolean; /** Wether the user gender should be visible or not */ @Property({ onCreate: () => false }) - @ApiProperty({ type: Boolean, default: false }) pronouns: boolean; + // TODO: (KEY: 1) Make a PR to implement cursus & specialty in the API /** Wether the user cursus should be visible or not */ // @Property({ onCreate: () => true }) // @ApiProperty({ type: Boolean, default: true }) @@ -51,16 +43,13 @@ export class UserVisibility extends BaseEntity implements UserVisibilityEntity true }) - @ApiProperty({ type: Boolean, default: true }) promotion: boolean; /** Wether the user phone should be visible or not */ @Property({ onCreate: () => false }) - @ApiProperty({ type: Boolean, default: false }) phone: boolean; /** Wether the user parent's contact should be visible or not */ @Property({ onCreate: () => false }) - @ApiProperty({ type: Boolean, default: false }) parent_contact: boolean; } diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 3406130f..46545717 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -1,8 +1,6 @@ import type { email } from '#types'; -import type { UserEntity } from '#types/api'; import { Cascade, Collection, Entity, ManyToMany, ManyToOne, OneToMany, OneToOne, Property } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; import { USER_GENDER } from '@exported/api/constants/genders'; import { BaseEntity } from '@modules/_mixin/entities/base.entity'; @@ -11,7 +9,6 @@ import { Log } from '@modules/logs/entities/log.entity'; import { Permission } from '@modules/permissions/entities/permission.entity'; import { Promotion } from '@modules/promotions/entities/promotion.entity'; import { Role } from '@modules/roles/entities/role.entity'; -import { Subscription } from '@modules/subscription/entities/subscription.entity'; import { UserBanner } from './user-banner.entity'; import { UserPicture } from './user-picture.entity'; @@ -23,41 +20,35 @@ export type UserPrivateKeys = Omit; export type UserPrivate = User; export type UserPublic = Omit & Pick; +export type RequestWithUser = Request & { + user: User; +}; + @Entity({ tableName: 'users' }) -export class User - extends BaseEntity - implements UserEntity -{ +export class User extends BaseEntity { //* INFORMATIONS @Property() - @ApiProperty({ example: 'John' }) first_name: string; @Property() - @ApiProperty({ example: 'Doe' }) last_name: string; /** Get the full name of the user */ @Property({ persist: false }) - @ApiProperty({ example: 'John Doe' }) get full_name(): string { return `${this.first_name} ${this.last_name}`; } @OneToOne(() => UserPicture, (picture) => picture.picture_user, { cascade: [Cascade.ALL], nullable: true }) - @ApiProperty({ type: Number, minimum: 1 }) picture?: UserPicture; @OneToOne(() => UserBanner, (banner) => banner.banner_user, { cascade: [Cascade.ALL], nullable: true }) - @ApiProperty({ type: Number, minimum: 1 }) banner?: UserBanner; @Property({ unique: true, type: String }) - @ApiProperty({ type: String, example: 'example@domain.com' }) email: email; @Property({ onCreate: () => false }) - @ApiProperty({ type: Boolean, default: false }) email_verified: boolean; @Property({ nullable: true, hidden: true }) @@ -67,12 +58,10 @@ export class User password: string; @Property({ type: 'date' }) - @ApiProperty({ example: new Date('1999-12-31').toISOString() }) birth_date: Date; /** The age of the user */ @Property({ persist: false }) - @ApiProperty({ minimum: 13 }) get age(): number { const diff = Date.now() - (this.birth_date instanceof Date ? this.birth_date : new Date(this.birth_date)).getTime(); const age = new Date(diff); @@ -80,58 +69,46 @@ export class User } @Property({ persist: false }) - @ApiProperty() get is_minor(): boolean { return this.age < 18; } @Property({ nullable: true }) - @ApiProperty() nickname?: string; @Property({ default: USER_GENDER[0] }) - @ApiProperty({ example: USER_GENDER[0], enum: USER_GENDER }) gender?: (typeof USER_GENDER)[number]; @Property({ nullable: true }) - @ApiProperty({ example: null }) pronouns?: string; // TODO: (KEY: 1) Make a PR to implement cursus & specialty in the API //* Should be a One to Many relation (one user can have multiple semester) // @Property({ nullable: true }) - // @ApiProperty() // cursus?: string; // @Property({ nullable: true }) - // @ApiProperty() // specialty?: string; @ManyToOne(() => Promotion, { nullable: true }) - @ApiProperty({ type: Number, minimum: 1 }) promotion?: Promotion; @Property({ type: 'date', nullable: true, onCreate: () => new Date() }) - @ApiProperty({ example: new Date().toISOString() }) last_seen?: Date; //* SUBSCRIPTIONS // TODO: (KEY: 2) Make a PR to implement subscriptions in the API @Property({ onCreate: () => false, hidden: true }) - @ApiProperty({ example: false }) subscribed: boolean; //* CONTACT @Property({ nullable: true }) - @ApiProperty() - secondary_email?: string; + secondary_email?: email; @Property({ nullable: true }) - @ApiProperty() phone?: string; @Property({ nullable: true }) - @ApiProperty() parent_contact?: string; //* PERMISSIONS & TRACKING @@ -149,7 +126,6 @@ export class User logs? = new Collection(this); @Property({ type: Date, nullable: true, onCreate: () => null }) - @ApiProperty({ type: Date }) verified?: Date; //* FILES diff --git a/src/modules/users/services/users-data.service.ts b/src/modules/users/services/users-data.service.ts index c43722d6..d14b73fb 100644 --- a/src/modules/users/services/users-data.service.ts +++ b/src/modules/users/services/users-data.service.ts @@ -9,10 +9,10 @@ import { I18nContext, I18nService } from 'nestjs-i18n'; import { z } from 'zod'; import { env } from '@env'; -import { MessageResponseDTO } from '@modules/_mixin/dto/message-response.dto'; -import { UserPostByAdminDTO, UserPostDTO } from '@modules/auth/dto/register.dto'; +import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; +import { CreateUserDTO, UserPostDTO } from '@modules/auth/dto/post.dto'; import { EmailsService } from '@modules/emails/emails.service'; -import { Permission } from '@modules/permissions/entities/permission.entity'; +import { PermissionGetDTO } from '@modules/permissions/dto/get.dto'; import { RoleExpiration } from '@modules/roles/entities/role-expiration.entity'; import { TranslateService } from '@modules/translate/translate.service'; import { UserVisibility } from '@modules/users/entities/user-visibility.entity'; @@ -22,7 +22,7 @@ import { checkPasswordStrength, generateRandomPassword } from '@utils/password'; import { getTemplate } from '@utils/template'; import { BaseUserResponseDTO } from '../dto/base-user.dto'; -import { UserRolesGetDTO } from '../dto/get.dto'; +import { UserGetDTO, UserRoleGetDTO, UserVisibilityGetDTO } from '../dto/get.dto'; import { UserPatchDTO, UserVisibilityPatchDTO } from '../dto/patch.dto'; @Injectable() @@ -63,7 +63,7 @@ export class UsersDataService { const visibilities = await this.findVisibilities(users.map((u) => u.id)); visibilities.forEach((v) => { - const user = users.find((u) => u.id === v.user.id); + const user = users.find((u) => u.id === v.user); Object.entries(v).forEach(([key, value]) => { // FIXME: Element implicitly has an 'any' type because expression of type 'string' @@ -139,23 +139,29 @@ export class UsersDataService { return filter ? (await this.removePrivateFields([user]))[0] : user; } + async findOneAsDTO(id_or_email: number | email, filter: boolean = true): Promise { + const user = await this.findOne(id_or_email, filter as false); + return { ...user, picture: user.picture?.id, banner: user.banner?.id, promotion: user.promotion?.id }; + } + /** - * Return the visibility parameters of a user + * Return the visibility parameters of given users * @param {number} ids The ids of the users - * @returns {Promise} The visibility parameters of each user + * @returns {Promise} The visibility parameters of each user */ @CreateRequestContext() - async findVisibilities(ids: number[] | number): Promise { + async findVisibilities(ids: number[] | number): Promise { if (!Array.isArray(ids)) ids = [ids]; const users = await this.orm.em.find(User, { id: { $in: ids } }); if (!users || users.length === 0) throw new NotFoundException(this.t.Errors.Id.NotFounds(User, ids)); - return await this.orm.em.find(UserVisibility, { user: { $in: users } }); + const visibilities = await this.orm.em.find(UserVisibility, { user: { $in: users } }); + return visibilities.map((v) => ({ ...v, user: v.user.id })); } @CreateRequestContext() - async updateVisibility(id: number, input: UserVisibilityPatchDTO): Promise { + async updateVisibility(id: number, input: UserVisibilityPatchDTO): Promise { const user = await this.orm.em.findOne(User, { id }); if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); @@ -167,11 +173,11 @@ export class UsersDataService { Object.assign(visibility, input); await this.orm.em.persistAndFlush(visibility); - return visibility; + return { ...visibility, user: visibility.user.id }; } @CreateRequestContext() - async register(input: UserPostDTO): Promise { + async register(input: UserPostDTO): Promise { Object.entries(input).forEach(([key, value]) => { if (typeof value === 'string') { // @ts-ignore @@ -200,7 +206,7 @@ export class UsersDataService { // Save changes to the database & create the user's visibility parameters this.orm.em.create(UserVisibility, { user }); - return user; + return { message: 'this.t.Success.User.Register(registerDto.email)', statusCode: 200 }; } @CreateRequestContext() @@ -244,7 +250,7 @@ export class UsersDataService { } @CreateRequestContext() - async registerByAdmin(inputs: UserPostByAdminDTO[]): Promise { + async registerByAdmin(inputs: CreateUserDTO[]): Promise { const existing_users = await this.orm.em.find(User, { email: { $in: inputs.map((i) => i.email) } }); if (existing_users.length > 0) throw new BadRequestException( @@ -283,7 +289,7 @@ export class UsersDataService { await this.orm.em.persistAndFlush(user); } - return users; + return this.asBaseUsers(users); } @CreateRequestContext() @@ -301,12 +307,12 @@ export class UsersDataService { await this.orm.em.persistAndFlush(user); return { message: this.t.Success.Email.Verified(user.email), - status_code: 200, + statusCode: 200, }; } @CreateRequestContext() - async update(requestUserId: number, inputs: UserPatchDTO[]) { + async update(requestUserId: number, inputs: UserPatchDTO[]): Promise { const users: User[] = []; for (const input of inputs) { @@ -331,11 +337,11 @@ export class UsersDataService { users.push(user); } - return users; + return users.map((u) => ({ ...u, picture: u.picture?.id, banner: u.banner?.id, promotion: u.promotion?.id })); } @CreateRequestContext() - async delete(id: number) { + async delete(id: number): Promise { const user = await this.orm.em.findOne(User, { id }); await this.orm.em.removeAndFlush(user); @@ -343,7 +349,7 @@ export class UsersDataService { } @CreateRequestContext() - async getUserRoles(id: number, input: { show_expired: boolean; show_revoked: boolean }): Promise { + async getUserRoles(id: number, input: { show_expired: boolean; show_revoked: boolean }): Promise { const user = await this.orm.em.findOne(User, { id }, { populate: ['roles'] }); if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); @@ -368,7 +374,10 @@ export class UsersDataService { } @CreateRequestContext() - async getUserPermissions(id: number, input: { show_expired: boolean; show_revoked: boolean }): Promise { + async getUserPermissions( + id: number, + input: { show_expired: boolean; show_revoked: boolean }, + ): Promise { const user = await this.orm.em.findOne(User, { id }, { populate: ['permissions'] }); if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); @@ -377,7 +386,7 @@ export class UsersDataService { if (!input.show_expired) permissions.filter((p) => p.expires > new Date()); if (!input.show_revoked) permissions.filter((p) => p.revoked === false); - return permissions; + return permissions.map((p) => ({ ...p, user: id })); } /** diff --git a/src/modules/users/services/users-files.service.ts b/src/modules/users/services/users-files.service.ts index 80055d44..4dd0730a 100644 --- a/src/modules/users/services/users-files.service.ts +++ b/src/modules/users/services/users-files.service.ts @@ -4,10 +4,12 @@ import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { env } from '@env'; +import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; import { FilesService } from '@modules/files/files.service'; import { TranslateService } from '@modules/translate/translate.service'; import { UsersDataService } from './users-data.service'; +import { UserGetBannerDTO, UserGetPictureDTO } from '../dto/get.dto'; import { UserBanner } from '../entities/user-banner.entity'; import { UserPicture } from '../entities/user-picture.entity'; import { User } from '../entities/user.entity'; @@ -23,17 +25,13 @@ export class UsersFilesService { /** * Edit user profile picture - * @param {User} req_id User making the request + * @param {User} req_user User making the request * @param {number} owner_id User id to whom the picture belongs * @param {Express.Multer.File} file The picture file * @returns {Promise} The updated user */ @CreateRequestContext() - async updatePicture( - req_user: User, - owner_id: number, - file: Express.Multer.File, - ): Promise> { + async updatePicture(req_user: User, owner_id: number, file: Express.Multer.File): Promise { const user = await this.orm.em.findOne(User, { id: owner_id }, { populate: ['picture'] }); if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, owner_id)); @@ -84,7 +82,8 @@ export class UsersFilesService { delete user.picture.picture_user; // avoid circular reference delete user.picture.visibility; - return user.picture; + + return { ...user.picture, picture_user: user.id, visibility: user.picture.visibility.id }; } @CreateRequestContext() @@ -97,7 +96,7 @@ export class UsersFilesService { } @CreateRequestContext() - async deletePicture(id: number): Promise { + async deletePicture(id: number): Promise { const user = await this.orm.em.findOne(User, { id }, { populate: ['picture'] }); if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); if (!user.picture) throw new NotFoundException(this.t.Errors.User.NoPicture(id)); @@ -105,11 +104,11 @@ export class UsersFilesService { this.filesService.deleteFromDisk(user.picture); await this.orm.em.removeAndFlush(user.picture); - return user; + return { message: this.t.Success.Entity.Deleted(UserPicture), statusCode: 200 }; } @CreateRequestContext() - async updateBanner(id: number, file: Express.Multer.File): Promise { + async updateBanner(id: number, file: Express.Multer.File): Promise { const user = await this.orm.em.findOne(User, { id }, { populate: ['banner'] }); if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); @@ -145,7 +144,7 @@ export class UsersFilesService { delete user.banner.banner_user; // avoid circular reference delete user.banner.visibility; - return user.banner; + return { ...user.banner, banner_user: user.id, visibility: user.banner.visibility.id }; } @CreateRequestContext() @@ -158,7 +157,7 @@ export class UsersFilesService { } @CreateRequestContext() - async deleteBanner(id: number): Promise { + async deleteBanner(id: number): Promise { const user = await this.orm.em.findOne(User, { id }, { populate: ['banner'] }); if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); if (!user.banner) throw new NotFoundException(this.t.Errors.User.NoBanner(id)); @@ -166,6 +165,6 @@ export class UsersFilesService { this.filesService.deleteFromDisk(user.banner); await this.orm.em.removeAndFlush(user.banner); - return user; + return { message: this.t.Success.Entity.Deleted(UserBanner), statusCode: 200 }; } } diff --git a/tests/e2e/auth.e2e-spec.ts b/tests/e2e/auth.e2e-spec.ts index 9ac5d8a1..c77f97f7 100644 --- a/tests/e2e/auth.e2e-spec.ts +++ b/tests/e2e/auth.e2e-spec.ts @@ -4,7 +4,7 @@ import { hashSync } from 'bcrypt'; import request from 'supertest'; import { USER_GENDER } from '@exported/api/constants/genders'; -import { UserPostDTO } from '@modules/auth/dto/register.dto'; +import { UserPostDTO } from '@modules/auth/dto/post.dto'; import { User } from '@modules/users/entities/user.entity'; import { generateRandomPassword } from '@utils/password'; diff --git a/tests/e2e/logs.e2e-spec.ts b/tests/e2e/logs.e2e-spec.ts index 17da3918..b71a188a 100644 --- a/tests/e2e/logs.e2e-spec.ts +++ b/tests/e2e/logs.e2e-spec.ts @@ -1,6 +1,6 @@ import request from 'supertest'; -import { TokenDTO } from '@modules/auth/dto/token.dto'; +import { TokenDTO } from '@modules/auth/dto/get.dto'; import { Log } from '@modules/logs/entities/log.entity'; import { User } from '@modules/users/entities/user.entity'; diff --git a/tests/e2e/permissions.e2e-spec.ts b/tests/e2e/permissions.e2e-spec.ts index eeb10a5c..cdeaf574 100644 --- a/tests/e2e/permissions.e2e-spec.ts +++ b/tests/e2e/permissions.e2e-spec.ts @@ -1,6 +1,6 @@ import request from 'supertest'; -import { TokenDTO } from '@modules/auth/dto/token.dto'; +import { TokenDTO } from '@modules/auth/dto/get.dto'; import { Permission } from '@modules/permissions/entities/permission.entity'; import { User } from '@modules/users/entities/user.entity'; diff --git a/tests/e2e/promotions.e2e-spec.ts b/tests/e2e/promotions.e2e-spec.ts index 2ff3c3a1..29a1bc9b 100644 --- a/tests/e2e/promotions.e2e-spec.ts +++ b/tests/e2e/promotions.e2e-spec.ts @@ -4,7 +4,7 @@ import { join } from 'path'; import request from 'supertest'; import { env } from '@env'; -import { TokenDTO } from '@modules/auth/dto/token.dto'; +import { TokenDTO } from '@modules/auth/dto/get.dto'; import { PromotionPicture } from '@modules/promotions/entities/promotion-picture.entity'; import { Promotion } from '@modules/promotions/entities/promotion.entity'; diff --git a/tests/e2e/users/users-data.e2e-spec.ts b/tests/e2e/users/users-data.e2e-spec.ts index d7afcc5d..6168c740 100644 --- a/tests/e2e/users/users-data.e2e-spec.ts +++ b/tests/e2e/users/users-data.e2e-spec.ts @@ -3,7 +3,7 @@ import type { email } from '#types'; import request from 'supertest'; import { USER_GENDER } from '@exported/api/constants/genders'; -import { TokenDTO } from '@modules/auth/dto/token.dto'; +import { TokenDTO } from '@modules/auth/dto/get.dto'; import { User } from '@modules/users/entities/user.entity'; import { orm, t, server } from '../..'; diff --git a/tests/e2e/users/users-files.e2e-spec.ts b/tests/e2e/users/users-files.e2e-spec.ts index b751255b..e36435bc 100644 --- a/tests/e2e/users/users-files.e2e-spec.ts +++ b/tests/e2e/users/users-files.e2e-spec.ts @@ -3,7 +3,7 @@ import { join } from 'path'; import request from 'supertest'; -import { TokenDTO } from '@modules/auth/dto/token.dto'; +import { TokenDTO } from '@modules/auth/dto/get.dto'; import { FileVisibilityGroup } from '@modules/files/entities/file-visibility.entity'; import { UserBanner } from '@modules/users/entities/user-banner.entity'; import { UserPicture } from '@modules/users/entities/user-picture.entity'; From 41d35814828594c8aa181e83657ab00a00a083ea Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Wed, 1 Nov 2023 23:38:13 +0100 Subject: [PATCH 02/15] refactor(swagger): ordered & improved decorators usage over endpoints --- src/exported | 2 +- .../_mixin/decorators/error.decorator.ts | 18 +++++ src/modules/_mixin/dto/error.dto.ts | 4 +- src/modules/auth/auth.controller.ts | 45 +++++------ .../auth/decorators/permissions.decorator.ts | 10 ++- .../self-or-subscribed.decorator.ts | 2 + .../files/decorators/download.decorator.ts | 14 ++++ .../files/decorators/upload.decorator.ts | 21 ++++++ src/modules/logs/logs.controller.ts | 30 +++----- .../permissions/permissions.controller.ts | 32 +++----- .../permissions/permissions.service.ts | 5 +- .../promotions/promotions.controller.ts | 67 +++++------------ src/modules/roles/roles.controller.ts | 39 ++++------ .../controllers/users-data.controller.ts | 37 +++++----- .../controllers/users-files.controller.ts | 74 ++++++------------- src/modules/users/entities/user.entity.ts | 2 +- 16 files changed, 187 insertions(+), 215 deletions(-) create mode 100644 src/modules/_mixin/decorators/error.decorator.ts create mode 100644 src/modules/files/decorators/download.decorator.ts create mode 100644 src/modules/files/decorators/upload.decorator.ts diff --git a/src/exported b/src/exported index 4f920643..e54c388b 160000 --- a/src/exported +++ b/src/exported @@ -1 +1 @@ -Subproject commit 4f920643fef209a20c927e0d0970466e8c6fba4a +Subproject commit e54c388b53cc72cc8c7534014527a86b3dd0d152 diff --git a/src/modules/_mixin/decorators/error.decorator.ts b/src/modules/_mixin/decorators/error.decorator.ts new file mode 100644 index 00000000..fcea1daa --- /dev/null +++ b/src/modules/_mixin/decorators/error.decorator.ts @@ -0,0 +1,18 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiResponse } from '@nestjs/swagger'; + +type HttpStatus = 400 | 401 | 403 | 404; +type ApiHttpErrors = { + [key in HttpStatus]: string; +}; + +export function ApiNotOkResponses(errors: Partial) { + return applyDecorators( + ...Object.keys(errors).map((key) => { + return ApiResponse({ + description: errors[key], + status: key, + }); + }), + ); +} diff --git a/src/modules/_mixin/dto/error.dto.ts b/src/modules/_mixin/dto/error.dto.ts index 65446298..4ec4858d 100644 --- a/src/modules/_mixin/dto/error.dto.ts +++ b/src/modules/_mixin/dto/error.dto.ts @@ -4,11 +4,11 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString, IsInt } from 'class-validator'; export class ErrorResponseDTO implements IErrorResponseDTO { - @ApiProperty() + @ApiProperty({ example: 'Bad Request' }) @IsString() error: string; - @ApiProperty() + @ApiProperty({ required: false }) @IsString() message: string; diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 011e3e37..ad90d872 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,17 +1,8 @@ import { Controller, Post, Body, Param, Get } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; +import { ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { z } from 'zod'; -import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; import { TranslateService } from '@modules/translate/translate.service'; import { User } from '@modules/users/entities/user.entity'; @@ -33,11 +24,14 @@ export class AuthController { @Post('login') @ApiOperation({ summary: 'Sign in a user with email and password' }) - @ApiUnauthorizedResponse({ description: 'Unauthorized, password invalid', type: ErrorResponseDTO }) - @ApiForbiddenResponse({ description: 'Forbidden, email not verified', type: ErrorResponseDTO }) - @ApiNotFoundResponse({ description: 'User not found', type: ErrorResponseDTO }) @ApiOkResponse({ description: 'OK', type: TokenDTO }) - async login(@Body() signInDto: SignInDTO) { + @ApiNotOkResponses({ + 400: 'Bad Request, invalid fields', + 401: 'Unauthorized, password mismatch', + 403: 'Forbidden, email not verified', + 404: 'User not found', + }) + async login(@Body() signInDto: SignInDTO): Promise { const schema = z .object({ password: z.string(), @@ -53,8 +47,10 @@ export class AuthController { @Post('register') @ApiOperation({ summary: 'Register a new user' }) @ApiOkResponse({ description: 'User created', type: MessageResponseDTO }) - @ApiBadRequestResponse({ description: 'Bad request, invalid fields', type: ErrorResponseDTO }) - async register(@Body() registerDto: UserPostDTO) { + @ApiNotOkResponses({ + 400: 'Bad request, invalid fields', + }) + async register(@Body() registerDto: UserPostDTO): Promise { const schema = z .object({ password: z.string(), @@ -71,16 +67,15 @@ export class AuthController { } @Get('confirm/:user_id/:token') - @ApiParam({ name: 'user_id', type: Number }) - @ApiParam({ name: 'token', type: String }) @ApiOperation({ summary: 'Validate a user account and redirect after' }) - @ApiNotFoundResponse({ description: 'User not found', type: ErrorResponseDTO }) - @ApiBadRequestResponse({ - description: 'Missing id/token or email already verified', - type: ErrorResponseDTO, - }) - @ApiUnauthorizedResponse({ description: 'Unauthorized, invalid token', type: ErrorResponseDTO }) + @ApiParam({ name: 'user_id', description: 'The user ID' }) + @ApiParam({ name: 'token', description: 'The email verification token' }) @ApiOkResponse({ description: 'OK', type: MessageResponseDTO }) + @ApiNotOkResponses({ + 400: 'Missing ID/token or email already verified', + 401: 'Unauthorized, invalid token', + 404: 'User not found', + }) async verifyEmailAndRedirect(@Param('user_id') user_id: number, @Param('token') token: string) { validate(z.coerce.number().int().min(1), user_id, this.t.Errors.Id.Invalid(User, user_id)); validate(z.string().min(12), token, this.t.Errors.JWT.Invalid()); diff --git a/src/modules/auth/decorators/permissions.decorator.ts b/src/modules/auth/decorators/permissions.decorator.ts index 05ddbf86..98144eb6 100644 --- a/src/modules/auth/decorators/permissions.decorator.ts +++ b/src/modules/auth/decorators/permissions.decorator.ts @@ -1,9 +1,15 @@ import type { PERMISSION_NAMES } from '#types/api'; -import { SetMetadata } from '@nestjs/common'; +import { SetMetadata, applyDecorators } from '@nestjs/common'; +import { ApiForbiddenResponse, ApiUnauthorizedResponse } from '@nestjs/swagger'; /** * Set up what permissions are required to access the decorated route * @param {...PERMISSION_NAMES} permissions - list of permissions required to access the route */ -export const GuardPermissions = (...permissions: PERMISSION_NAMES[]) => SetMetadata('guard_permissions', permissions); +export const GuardPermissions = (...permissions: PERMISSION_NAMES[]) => + applyDecorators( + SetMetadata('guard_permissions', permissions), + ApiForbiddenResponse({ description: 'Forbidden, missing permissions' }), + ApiUnauthorizedResponse({ description: 'Unauthorized, missing authentification token' }), + ); diff --git a/src/modules/auth/decorators/self-or-subscribed.decorator.ts b/src/modules/auth/decorators/self-or-subscribed.decorator.ts index 1b21dff2..ead29459 100644 --- a/src/modules/auth/decorators/self-or-subscribed.decorator.ts +++ b/src/modules/auth/decorators/self-or-subscribed.decorator.ts @@ -3,5 +3,7 @@ import { SetMetadata } from '@nestjs/common'; /** * Set up the name of the parameter that contains the user id concerned by the route * @param {string} param The name of the parameter that contains the user id + * + * TODO: (KEY: 2) Make a PR to implement subscriptions in the API */ export const GuardSelfOrSubscribed = (param: string) => SetMetadata('guard_self_param_key', param); diff --git a/src/modules/files/decorators/download.decorator.ts b/src/modules/files/decorators/download.decorator.ts new file mode 100644 index 00000000..2aa7705a --- /dev/null +++ b/src/modules/files/decorators/download.decorator.ts @@ -0,0 +1,14 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiResponse } from '@nestjs/swagger'; + +export function ApiDownloadFile(description?: string) { + return applyDecorators( + ApiResponse({ + status: 200, + description, + content: { + 'application/octet-stream': {}, + }, + }), + ); +} diff --git a/src/modules/files/decorators/upload.decorator.ts b/src/modules/files/decorators/upload.decorator.ts new file mode 100644 index 00000000..b318fb0a --- /dev/null +++ b/src/modules/files/decorators/upload.decorator.ts @@ -0,0 +1,21 @@ +import { UseInterceptors, applyDecorators } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiConsumes, ApiBody } from '@nestjs/swagger'; + +export function ApiUploadFile() { + return applyDecorators( + ApiConsumes('multipart/form-data'), + ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + }, + }, + }), + UseInterceptors(FileInterceptor('file')), + ); +} diff --git a/src/modules/logs/logs.controller.ts b/src/modules/logs/logs.controller.ts index 08f27d47..cfbbbb3f 100644 --- a/src/modules/logs/logs.controller.ts +++ b/src/modules/logs/logs.controller.ts @@ -1,17 +1,9 @@ import { Controller, Delete, Get, Param, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { - ApiTags, - ApiBearerAuth, - ApiNotFoundResponse, - ApiBadRequestResponse, - ApiOkResponse, - ApiParam, - ApiOperation, -} from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOkResponse, ApiParam, ApiOperation } from '@nestjs/swagger'; import { z } from 'zod'; -import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; @@ -34,12 +26,11 @@ export class LogsController { @Get('user/:user_id') @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('user_id', ['CAN_READ_LOGS_OF_USER']) - @ApiNotFoundResponse({ description: 'User not found', type: ErrorResponseDTO }) - @ApiBadRequestResponse({ description: 'Invalid user ID', type: ErrorResponseDTO }) - @ApiOkResponse({ description: 'User logs retrieved', type: [LogDTO] }) - @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOperation({ summary: 'Get all logs of a user' }) - getUserLogs(@Param('user_id') id: number) { + @ApiParam({ name: 'id', description: 'The user ID' }) + @ApiOkResponse({ description: 'User logs retrieved', type: [LogDTO] }) + @ApiNotOkResponses({ 400: 'Invalid user ID', 404: 'User not found' }) + async getUserLogs(@Param('user_id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); return this.logsService.getUserLogs(id); @@ -48,12 +39,11 @@ export class LogsController { @Delete('user/:user_id') @UseGuards(PermissionGuard) @GuardPermissions('CAN_DELETE_LOGS_OF_USER') - @ApiNotFoundResponse({ description: 'User not found', type: ErrorResponseDTO }) - @ApiBadRequestResponse({ description: 'Invalid user ID', type: ErrorResponseDTO }) - @ApiOkResponse({ description: 'User logs deleted', type: MessageResponseDTO }) - @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOperation({ summary: 'Delete all logs of a user' }) - deleteUserLogs(@Param('user_id') id: number) { + @ApiParam({ name: 'id', description: 'The user ID' }) + @ApiOkResponse({ description: 'User logs deleted', type: MessageResponseDTO }) + @ApiNotOkResponses({ 400: 'Invalid user ID', 404: 'User not found' }) + async deleteUserLogs(@Param('user_id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); return this.logsService.deleteUserLogs(id); diff --git a/src/modules/permissions/permissions.controller.ts b/src/modules/permissions/permissions.controller.ts index 2da22ad7..506c48f1 100644 --- a/src/modules/permissions/permissions.controller.ts +++ b/src/modules/permissions/permissions.controller.ts @@ -1,17 +1,9 @@ import { Body, Controller, Get, Param, Post, UseGuards, Patch } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { - ApiBearerAuth, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { z } from 'zod'; -import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; @@ -37,9 +29,11 @@ export class PermissionsController { @GuardPermissions('CAN_EDIT_PERMISSIONS_OF_USER') @ApiOperation({ summary: 'Add a permission to a user' }) @ApiOkResponse({ description: 'The added permission', type: PermissionGetDTO }) - @ApiNotFoundResponse({ description: 'User not found', type: ErrorResponseDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - addToUser(@Body() body: PermissionPostDTO) { + @ApiNotOkResponses({ + 400: 'Bad request, invalid fields', + 404: 'User not found', + }) + async addToUser(@Body() body: PermissionPostDTO): Promise { const schema = z .object({ expires: z.string().datetime(), @@ -56,10 +50,9 @@ export class PermissionsController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_PERMISSIONS_OF_USER') @ApiOperation({ summary: 'Edit permission of a user' }) - @ApiNotFoundResponse({ description: 'User not found', type: ErrorResponseDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) @ApiOkResponse({ description: 'The modified user permission', type: PermissionGetDTO }) - editPermissionFromUser(@Body() body: PermissionPatchDTO) { + @ApiNotOkResponses({ 404: 'User/permission not found' }) + async editPermissionFromUser(@Body() body: PermissionPatchDTO): Promise { const schema = z .object({ id: z.number().int().min(1), @@ -81,11 +74,10 @@ export class PermissionsController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('user_id', ['CAN_READ_PERMISSIONS_OF_USER']) @ApiOperation({ summary: 'Get all permissions of a user (active, revoked and expired)' }) - @ApiNotFoundResponse({ description: 'User not found' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - @ApiOkResponse({ description: 'User permission(s) retrieved', type: [PermissionGetDTO] }) @ApiParam({ name: 'user_id', description: 'The user ID' }) - getUserPermissions(@Param('user_id') id: number) { + @ApiOkResponse({ description: 'User permission(s) retrieved', type: [PermissionGetDTO] }) + @ApiNotOkResponses({ 404: 'User not found' }) + async getUserPermissions(@Param('user_id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); return this.permsService.getPermissionsOfUser(id); diff --git a/src/modules/permissions/permissions.service.ts b/src/modules/permissions/permissions.service.ts index 3065ba01..cd575227 100644 --- a/src/modules/permissions/permissions.service.ts +++ b/src/modules/permissions/permissions.service.ts @@ -77,11 +77,12 @@ export class PermissionsService { * @returns {Promise} The permissions of the user */ @CreateRequestContext() - async getPermissionsOfUser(id: number): Promise { + async getPermissionsOfUser(id: number): Promise { const user = await this.orm.em.findOne(User, { id }); if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); - return user.permissions.loadItems(); + const permissions = await user.permissions.loadItems(); + return permissions.map((p) => ({ ...p, user: user.id })); } @CreateRequestContext() diff --git a/src/modules/promotions/promotions.controller.ts b/src/modules/promotions/promotions.controller.ts index 3374d4f6..388debb5 100644 --- a/src/modules/promotions/promotions.controller.ts +++ b/src/modules/promotions/promotions.controller.ts @@ -8,28 +8,17 @@ import { StreamableFile, UploadedFile, UseGuards, - UseInterceptors, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiBody, - ApiConsumes, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { z } from 'zod'; -import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; +import { ApiDownloadFile } from '@modules/files/decorators/download.decorator'; +import { ApiUploadFile } from '@modules/files/decorators/upload.decorator'; import { FilesService } from '@modules/files/files.service'; import { TranslateService } from '@modules/translate/translate.service'; import { validate } from '@utils/validate'; @@ -53,20 +42,18 @@ export class PromotionsController { @Get() @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_PROMOTION') + @ApiOperation({ summary: 'Get all existing promotions' }) @ApiOkResponse({ type: [PromotionResponseDTO] }) - @ApiOperation({ summary: 'Get all promotions' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) - async getAll() { + async getAll(): Promise { return this.promotionsService.findAll(); } @Get('latest') @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_PROMOTION') + @ApiOperation({ summary: 'Get the latest promotion that has been created' }) @ApiOkResponse({ type: PromotionResponseDTO }) - @ApiOperation({ summary: 'Get the latest promotion' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - async getLatest() { + async getLatest(): Promise { return this.promotionsService.findLatest(); } @@ -75,33 +62,18 @@ export class PromotionsController { @GuardPermissions('CAN_READ_PROMOTION') @ApiOperation({ summary: 'Get promotions currently active' }) @ApiOkResponse({ type: [PromotionResponseDTO] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - async getCurrent() { + async getCurrent(): Promise { return this.promotionsService.findCurrent(); } @Post(':number/logo') @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_PROMOTION') - @ApiConsumes('multipart/form-data') - @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOperation({ summary: 'Update the promotion logo' }) - @ApiNotFoundResponse({ description: 'Promotion not found', type: ErrorResponseDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - @ApiBadRequestResponse({ description: 'Invalid file', type: ErrorResponseDTO }) + @ApiUploadFile() + @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOkResponse({ type: PromotionResponseDTO }) - @ApiBody({ - schema: { - type: 'object', - properties: { - file: { - type: 'string', - format: 'binary', - }, - }, - }, - }) - @UseInterceptors(FileInterceptor('file')) + @ApiNotOkResponses({ 400: 'Invalid file', 404: 'Promotion not found' }) async editLogo(@UploadedFile() file: Express.Multer.File, @Param('number') number: number) { if (!file) throw new BadRequestException(this.t.Errors.File.NotProvided()); validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); @@ -112,10 +84,10 @@ export class PromotionsController { @Get(':number/logo') @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_PROMOTION') - @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOperation({ summary: 'Get the promotion logo' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - @ApiNotFoundResponse({ description: 'Promotion not found or promotion has no logo', type: ErrorResponseDTO }) + @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) + @ApiDownloadFile('The promotion logo') + @ApiNotOkResponses({ 404: 'Promotion not found or promotion has no logo' }) async getLogo(@Param('number') number: number) { validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); @@ -129,8 +101,7 @@ export class PromotionsController { @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOperation({ summary: 'Delete the promotion logo' }) @ApiOkResponse({ type: MessageResponseDTO }) - @ApiNotFoundResponse({ description: 'Promotion not found', type: ErrorResponseDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiNotOkResponses({ 404: 'Promotion not found or promotion has no logo' }) async deleteLogo(@Param('number') number: number) { validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); @@ -143,8 +114,7 @@ export class PromotionsController { @ApiOperation({ summary: 'Get the specified promotion' }) @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOkResponse({ type: PromotionResponseDTO }) - @ApiNotFoundResponse({ description: 'Promotion not found', type: ErrorResponseDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiNotOkResponses({ 404: 'Promotion not found' }) async get(@Param('number') number: number) { validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); @@ -157,8 +127,7 @@ export class PromotionsController { @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOperation({ summary: 'Get users of the specified promotions' }) @ApiOkResponse({ type: [BaseUserResponseDTO] }) - @ApiNotFoundResponse({ description: 'Promotion not found', type: ErrorResponseDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiNotOkResponses({ 404: 'Promotion not found' }) async getUsers(@Param('number') number: number) { validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); diff --git a/src/modules/roles/roles.controller.ts b/src/modules/roles/roles.controller.ts index 9aede43a..489cb413 100644 --- a/src/modules/roles/roles.controller.ts +++ b/src/modules/roles/roles.controller.ts @@ -1,18 +1,9 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { - ApiTags, - ApiBearerAuth, - ApiOkResponse, - ApiUnauthorizedResponse, - ApiOperation, - ApiBadRequestResponse, - ApiNotFoundResponse, - ApiBody, -} from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiBody, ApiParam } from '@nestjs/swagger'; import { z } from 'zod'; -import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { TranslateService } from '@modules/translate/translate.service'; @@ -36,8 +27,7 @@ export class RolesController { @GuardPermissions('CAN_EDIT_ROLE') @ApiOperation({ summary: 'Create a new role' }) @ApiOkResponse({ type: RoleGetDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - @ApiBadRequestResponse({ description: 'Role name is not uppercase or already exists', type: ErrorResponseDTO }) + @ApiNotOkResponses({ 400: 'Role name is not uppercase or already exists' }) async createRole(@Body() body: RolePostDTO) { const schema = z .object({ @@ -55,9 +45,7 @@ export class RolesController { @GuardPermissions('CAN_EDIT_ROLE') @ApiOperation({ summary: 'Update an existing role' }) @ApiOkResponse({ type: RoleGetDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - @ApiBadRequestResponse({ description: 'Role name is not uppercase', type: ErrorResponseDTO }) - @ApiNotFoundResponse({ description: 'Role not found', type: ErrorResponseDTO }) + @ApiNotOkResponses({ 400: 'Role name is not uppercase', 404: 'Role not found' }) async editRole(@Body() body: RolePatchDTO) { const schema = z .object({ @@ -79,7 +67,6 @@ export class RolesController { @GuardPermissions('CAN_READ_ROLE') @ApiOperation({ summary: 'Get all existing roles' }) @ApiOkResponse({ type: [RoleGetDTO] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) async getAllRoles() { return this.rolesService.getAllRoles(); } @@ -88,9 +75,9 @@ export class RolesController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_ROLE') @ApiOperation({ summary: 'Get the specified role' }) + @ApiParam({ name: 'role_id', description: 'The role ID' }) @ApiOkResponse({ type: RoleGetDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - @ApiNotFoundResponse({ description: 'Role not found', type: ErrorResponseDTO }) + @ApiNotOkResponses({ 400: 'Invalid role ID', 404: 'Role not found' }) async getRole(@Param('role_id') id: number) { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(Role, id)); @@ -101,9 +88,9 @@ export class RolesController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_ROLE') @ApiOperation({ summary: 'Get user(s) of the specified role' }) + @ApiParam({ name: 'role_id', description: 'The role ID' }) @ApiOkResponse({ type: [RoleUsersResponseDTO] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - @ApiNotFoundResponse({ description: 'Role not found', type: ErrorResponseDTO }) + @ApiNotOkResponses({ 400: 'Invalid role ID', 404: 'Role not found' }) async getRoleUsers(@Param('role_id') id: number) { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(Role, id)); @@ -114,9 +101,9 @@ export class RolesController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_ROLE') @ApiOperation({ summary: 'Add user(s) to the role' }) + @ApiParam({ name: 'role_id', description: 'The role ID' }) @ApiOkResponse({ type: [RoleUsersResponseDTO] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - @ApiNotFoundResponse({ description: 'Role not found', type: ErrorResponseDTO }) + @ApiNotOkResponses({ 400: 'Invalid role ID or body', 404: 'Role not found' }) @ApiBody({ type: [RoleEditUserDTO] }) async addUsersToRole(@Param('role_id') role_id: number, @Body() body: RoleEditUserDTO[]) { validate(z.coerce.number().int().min(1), role_id, this.t.Errors.Id.Invalid(Role, role_id)); @@ -136,11 +123,11 @@ export class RolesController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_ROLE') @ApiOperation({ summary: 'Remove user(s) from the role' }) + @ApiParam({ name: 'role_id', description: 'The role ID' }) @ApiOkResponse({ type: [RoleUsersResponseDTO] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - @ApiNotFoundResponse({ description: 'Role not found', type: ErrorResponseDTO }) + @ApiNotOkResponses({ 400: 'Invalid role ID or given users IDs', 404: 'Role not found' }) @ApiBody({ type: [Number] }) - async removeUsersToRole(@Param('role_id') role_id: number, @Body('') body: number[]) { + async removeUsersToRole(@Param('role_id') role_id: number, @Body() body: number[]) { validate(z.coerce.number().int().min(1), role_id, this.t.Errors.Id.Invalid(Role, role_id)); const schema = z.array(z.number().min(1)).min(1); diff --git a/src/modules/users/controllers/users-data.controller.ts b/src/modules/users/controllers/users-data.controller.ts index f636b654..a248e16d 100644 --- a/src/modules/users/controllers/users-data.controller.ts +++ b/src/modules/users/controllers/users-data.controller.ts @@ -1,12 +1,10 @@ -import type { RequestWithUser } from '#types/api'; - import { Body, Controller, Delete, Get, Param, Patch, Post, Req, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { ApiBearerAuth, ApiBody, ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { z } from 'zod'; import { USER_GENDER } from '@exported/api/constants/genders'; -import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; @@ -24,7 +22,7 @@ import { validate } from '@utils/validate'; import { BaseUserResponseDTO } from '../dto/base-user.dto'; import { UserGetDTO, UserRoleGetDTO, UserVisibilityGetDTO } from '../dto/get.dto'; import { UserPatchDTO, UserVisibilityPatchDTO } from '../dto/patch.dto'; -import { User } from '../entities/user.entity'; +import { Request, User } from '../entities/user.entity'; import { UsersDataService } from '../services/users-data.service'; @ApiTags('Users') @@ -39,8 +37,7 @@ export class UsersDataController { @GuardPermissions('CAN_EDIT_USER') @ApiOperation({ summary: 'Creates new users' }) @ApiOkResponse({ description: 'The created user', type: [BaseUserResponseDTO] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - @ApiBody({ type: [CreateUserDTO] }) + @ApiNotOkResponses({ 400: 'Invalid input', 401: 'Insufficient permission' }) async create(@Body() input: CreateUserDTO[]): Promise { const schema = z .object({ @@ -61,9 +58,8 @@ export class UsersDataController { @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Update users data' }) @ApiOkResponse({ description: 'The updated users', type: UserGetDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - @ApiBody({ type: [UserPatchDTO] }) - async update(@Req() req: RequestWithUser, @Body() input: UserPatchDTO[]): Promise { + @ApiNotOkResponses({ 400: 'Invalid input', 404: 'User(s) not found' }) + async update(@Req() req: Request, @Body() input: UserPatchDTO[]): Promise { const schema = z .object({ id: z.coerce.number(), @@ -93,8 +89,9 @@ export class UsersDataController { @UseGuards(SelfGuard) @GuardSelfParam('id') @ApiOperation({ summary: 'Delete your account' }) + @ApiParam({ name: 'id', description: 'Your user ID' }) @ApiOkResponse({ description: 'User deleted', type: MessageResponseDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) async delete(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); @@ -105,8 +102,9 @@ export class UsersDataController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_READ_USER_PRIVATE']) @ApiOperation({ summary: 'Get all information of a user' }) + @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOkResponse({ description: 'User data', type: UserGetDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) async getPrivate(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); @@ -117,8 +115,9 @@ export class UsersDataController { @UseGuards(SelfOrPermsOrSubGuard) @GuardSelfOrPermsOrSub('id', ['CAN_READ_USER']) @ApiOperation({ summary: 'Get publicly available information of a user' }) + @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOkResponse({ description: 'User data, excepted privates fields (set in the visibility table)', type: UserGetDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) async getPublic(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); @@ -129,8 +128,9 @@ export class UsersDataController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_READ_USER_PRIVATE']) @ApiOperation({ summary: 'Get visibility settings of a user' }) + @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOkResponse({ description: 'User data', type: UserVisibilityGetDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) async getVisibility(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); @@ -141,8 +141,9 @@ export class UsersDataController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Update visibility settings of a user' }) + @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOkResponse({ description: 'User data', type: UserVisibilityGetDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiNotOkResponses({ 400: 'Invalid ID or input', 404: 'User not found' }) async updateVisibility( @Param('id') id: number, @Body() input: UserVisibilityPatchDTO, @@ -170,8 +171,9 @@ export class UsersDataController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_READ_USER', 'CAN_READ_ROLE']) @ApiOperation({ summary: 'Get roles of a user' }) + @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOkResponse({ description: 'Roles of the user', type: [UserRoleGetDTO] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) async getUserRoles(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); @@ -182,8 +184,9 @@ export class UsersDataController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_READ_USER', 'CAN_READ_PERMISSIONS_OF_USER']) @ApiOperation({ summary: 'Get permissions of a user' }) + @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOkResponse({ description: 'Permissions of the user', type: [PermissionGetDTO] }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission' }) + @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) async getUserPermissions(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); diff --git a/src/modules/users/controllers/users-files.controller.ts b/src/modules/users/controllers/users-files.controller.ts index 919344e8..345faf56 100644 --- a/src/modules/users/controllers/users-files.controller.ts +++ b/src/modules/users/controllers/users-files.controller.ts @@ -10,22 +10,12 @@ import { UnauthorizedException, UploadedFile, UseGuards, - UseInterceptors, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { - ApiBearerAuth, - ApiBody, - ApiConsumes, - ApiOkResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { z } from 'zod'; -import { ErrorResponseDTO } from '@modules/_mixin/dto/error.dto'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; @@ -33,12 +23,14 @@ import { GuardSelfOrPermsOrSub } from '@modules/auth/decorators/self-or-sub-perm import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { SelfOrPermissionGuard } from '@modules/auth/guards/self-or-perms.guard'; import { SelfOrPermsOrSubGuard } from '@modules/auth/guards/self-or-sub-or-perms.guard'; +import { ApiDownloadFile } from '@modules/files/decorators/download.decorator'; +import { ApiUploadFile } from '@modules/files/decorators/upload.decorator'; import { FilesService } from '@modules/files/files.service'; import { TranslateService } from '@modules/translate/translate.service'; import { validate } from '@utils/validate'; import { UserGetBannerDTO, UserGetPictureDTO } from '../dto/get.dto'; -import { RequestWithUser, User } from '../entities/user.entity'; +import { Request, User } from '../entities/user.entity'; import { UsersFilesService } from '../services/users-files.service'; @ApiTags('Users Files') @@ -56,23 +48,12 @@ export class UsersFilesController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Update user profile picture' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOkResponse({ description: 'The updated user picture', type: UserGetPictureDTO }) - @ApiConsumes('multipart/form-data') - @ApiBody({ - schema: { - type: 'object', - properties: { - file: { - type: 'string', - format: 'binary', - }, - }, - }, - }) - @UseInterceptors(FileInterceptor('file')) + @ApiNotOkResponses({ 400: 'Invalid user ID or missing uploaded file', 404: 'User not found' }) + @ApiUploadFile() async editPicture( - @Req() req: RequestWithUser, + @Req() req: Request, @UploadedFile() file: Express.Multer.File, @Param('id') id: number, ): Promise { @@ -86,8 +67,9 @@ export class UsersFilesController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_USER') @ApiOperation({ summary: 'Delete user profile picture' }) + @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOkResponse({ type: MessageResponseDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) async deletePicture(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); @@ -98,9 +80,10 @@ export class UsersFilesController { @UseGuards(SelfOrPermsOrSubGuard) @GuardSelfOrPermsOrSub('id', ['CAN_READ_USER']) @ApiOperation({ summary: 'Get user profile picture' }) - @ApiOkResponse({ description: 'The user picture', type: ArrayBuffer }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - async getPicture(@Req() req: RequestWithUser, @Param('id') id: number) { + @ApiParam({ name: 'id', description: 'The user ID' }) + @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) + @ApiDownloadFile() + async getPicture(@Req() req: Request, @Param('id') id: number) { validate(z.coerce.number().int().min(1), id); const picture = await this.usersFilesService.getPicture(id); @@ -119,21 +102,10 @@ export class UsersFilesController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Update user profile banner' }) + @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOkResponse({ description: 'The updated user banner', type: UserGetBannerDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - @ApiConsumes('multipart/form-data') - @ApiBody({ - schema: { - type: 'object', - properties: { - file: { - type: 'string', - format: 'binary', - }, - }, - }, - }) - @UseInterceptors(FileInterceptor('file')) + @ApiNotOkResponses({ 400: 'Invalid user ID or missing uploaded file', 404: 'User not found' }) + @ApiUploadFile() async editBanner(@UploadedFile() file: Express.Multer.File, @Param('id') id: number): Promise { if (!file) throw new BadRequestException(this.t.Errors.File.NotProvided()); validate(z.coerce.number().int().min(1), id); @@ -145,8 +117,9 @@ export class UsersFilesController { @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Delete user profile banner' }) + @ApiParam({ name: 'id', description: 'The user ID' }) @ApiOkResponse({ type: MessageResponseDTO }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) + @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) async deleteBanner(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); @@ -157,9 +130,10 @@ export class UsersFilesController { @UseGuards(SelfOrPermsOrSubGuard) @GuardSelfOrPermsOrSub('id', ['CAN_READ_USER']) @ApiOperation({ summary: 'Get user profile banner' }) - @ApiUnauthorizedResponse({ description: 'Insufficient permission', type: ErrorResponseDTO }) - @ApiOkResponse({ description: 'The user banner', type: ArrayBuffer }) - async getBanner(@Req() req: RequestWithUser, @Param('id') id: number) { + @ApiParam({ name: 'id', description: 'The user ID' }) + @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) + @ApiDownloadFile() + async getBanner(@Req() req: Request, @Param('id') id: number) { validate(z.coerce.number().int().min(1), id); const banner = await this.usersFilesService.getBanner(id); diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 46545717..f065a159 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -20,7 +20,7 @@ export type UserPrivateKeys = Omit; export type UserPrivate = User; export type UserPublic = Omit & Pick; -export type RequestWithUser = Request & { +export type Request = Express.Request & { user: User; }; From 615dbf7264cd2dcd5b05a9040583304183d09e8e Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Thu, 2 Nov 2023 02:35:37 +0100 Subject: [PATCH 03/15] fix(mikroorm): user serialized values to transform entities into DTOs --- src/exported | 2 +- src/i18n/en-US/responses.json | 3 + src/modules/_mixin/dto/message.dto.ts | 2 +- src/modules/auth/auth.service.ts | 14 ++- src/modules/auth/dto/post.dto.ts | 2 +- src/modules/files/dto/get.dto.ts | 4 +- .../files/entities/file-visibility.entity.ts | 13 +- src/modules/files/entities/file.entity.ts | 7 +- src/modules/logs/dto/get.dto.ts | 2 +- src/modules/logs/entities/log.entity.ts | 2 +- src/modules/logs/logs.service.ts | 2 +- src/modules/permissions/dto/get.dto.ts | 2 +- .../permissions/entities/permission.entity.ts | 7 +- .../permissions/permissions.service.ts | 6 +- src/modules/promotions/dto/get.dto.ts | 4 +- .../entities/promotion-picture.entity.ts | 9 +- .../promotions/entities/promotion.entity.ts | 13 +- src/modules/promotions/promotions.service.ts | 52 +++----- src/modules/roles/dto/get.dto.ts | 2 +- .../roles/entities/role-expiration.entity.ts | 4 +- src/modules/roles/entities/role.entity.ts | 6 +- src/modules/roles/roles.service.ts | 9 +- src/modules/translate/translate.service.ts | 3 + .../controllers/users-data.controller.ts | 7 +- src/modules/users/dto/get.dto.ts | 112 ++++++++++++++++-- .../users/entities/user-banner.entity.ts | 9 +- .../users/entities/user-picture.entity.ts | 9 +- .../users/entities/user-visibility.entity.ts | 7 +- src/modules/users/entities/user.entity.ts | 32 +++-- .../users/services/users-data.service.ts | 65 ++++------ .../users/services/users-files.service.ts | 15 +-- tests/e2e/auth.e2e-spec.ts | 27 +---- tests/e2e/logs.e2e-spec.ts | 19 +-- tests/e2e/permissions.e2e-spec.ts | 6 +- tests/e2e/promotions.e2e-spec.ts | 74 +++++------- tests/e2e/roles.e2e-spec.ts | 7 +- tests/e2e/users/users-data.e2e-spec.ts | 52 +------- tests/e2e/users/users-files.e2e-spec.ts | 58 +++------ 38 files changed, 344 insertions(+), 325 deletions(-) diff --git a/src/exported b/src/exported index e54c388b..90076abb 160000 --- a/src/exported +++ b/src/exported @@ -1 +1 @@ -Subproject commit e54c388b53cc72cc8c7534014527a86b3dd0d152 +Subproject commit 90076abb92e4971e680cbd0194ccfa5876a17658 diff --git a/src/i18n/en-US/responses.json b/src/i18n/en-US/responses.json index 7b394257..793e0157 100644 --- a/src/i18n/en-US/responses.json +++ b/src/i18n/en-US/responses.json @@ -67,6 +67,9 @@ "deleted": "'{type}' successfully deleted.", "email": { "verified": "{type} email has been verified." + }, + "user": { + "register": "You have been successfully registered, please check your email to verify your account." } } } diff --git a/src/modules/_mixin/dto/message.dto.ts b/src/modules/_mixin/dto/message.dto.ts index b606869e..b03c8af6 100644 --- a/src/modules/_mixin/dto/message.dto.ts +++ b/src/modules/_mixin/dto/message.dto.ts @@ -7,7 +7,7 @@ import { IsInt, IsString } from 'class-validator'; * Message response DTO class (used to send a message to the client) * -> Mainly used for DELETE requests * - * @example { message: 'User successfully deleted', status_code: 200 } + * @example { message: 'User successfully deleted', statusCode: 200 } */ export class MessageResponseDTO implements IMessageResponseDTO { @ApiProperty() diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 6a3b0f73..dc77631f 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,14 +1,14 @@ import type { email } from '#types'; import type { JWTPayload } from '#types/api'; -import { ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'; +import { CreateRequestContext, MikroORM } from '@mikro-orm/core'; +import { ForbiddenException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { compareSync } from 'bcrypt'; import { env } from '@env'; import { TranslateService } from '@modules/translate/translate.service'; import { User } from '@modules/users/entities/user.entity'; -import { UsersDataService } from '@modules/users/services/users-data.service'; import { TokenDTO } from './dto/get.dto'; @@ -16,8 +16,8 @@ import { TokenDTO } from './dto/get.dto'; export class AuthService { constructor( private readonly t: TranslateService, + private readonly orm: MikroORM, private readonly jwtService: JwtService, - private readonly usersService: UsersDataService, ) {} /** @@ -26,8 +26,11 @@ export class AuthService { * @param {string} pass the user password (hashed or not) @default false * @returns {Promise} The JWT token and the user ID */ + @CreateRequestContext() async signIn(email: email, pass: string): Promise { - const user: User = await this.usersService.findOne(email, false); + const user: User = await this.orm.em.findOne(User, { email }, { fields: ['*', 'password'] }); + + if (!user) throw new NotFoundException(this.t.Errors.Email.NotFound(User, email)); if (user.password !== pass && !compareSync(pass, user.password)) { throw new UnauthorizedException(this.t.Errors.Password.Mismatch()); @@ -49,8 +52,9 @@ export class AuthService { * @param {JWTPayload} payload JWT Payload to validate * @returns {User} The user if found and valid, throw otherwise (email not verified) */ + @CreateRequestContext() async validateUser(payload: JWTPayload): Promise { - const user = await this.usersService.findOne(payload.email, false); + const user = await this.orm.em.findOne(User, { id: payload.sub }); // throw if user not verified if (!user.email_verified) throw new UnauthorizedException(this.t.Errors.Email.NotVerified(User)); diff --git a/src/modules/auth/dto/post.dto.ts b/src/modules/auth/dto/post.dto.ts index 972a5e57..8f734352 100644 --- a/src/modules/auth/dto/post.dto.ts +++ b/src/modules/auth/dto/post.dto.ts @@ -4,7 +4,7 @@ import type { ICreateUserDTO, ICreateUserByAdminDTO, ISignInDTO } from '#types/a import { ApiProperty } from '@nestjs/swagger'; import { IsDate, IsEmail, IsString, IsStrongPassword } from 'class-validator'; -export class CreateUserDTO implements Omit { +export class CreateUserDTO implements ICreateUserDTO { @ApiProperty({ example: 'example@domain.com' }) @IsEmail() email: email; diff --git a/src/modules/files/dto/get.dto.ts b/src/modules/files/dto/get.dto.ts index f6b152ac..c8684da0 100644 --- a/src/modules/files/dto/get.dto.ts +++ b/src/modules/files/dto/get.dto.ts @@ -52,9 +52,9 @@ export class FileVisibilityGroupGetDTO implements IFileVisibilityGroupGetDTO { @ApiProperty() @IsInt() - users: number; + users_count: number; @ApiProperty() @IsInt() - files: number; + files_count: number; } diff --git a/src/modules/files/entities/file-visibility.entity.ts b/src/modules/files/entities/file-visibility.entity.ts index 5a9347fc..c2739fd8 100644 --- a/src/modules/files/entities/file-visibility.entity.ts +++ b/src/modules/files/entities/file-visibility.entity.ts @@ -13,9 +13,18 @@ export class FileVisibilityGroup extends BaseEntity { description: string; //* Note: Used the 'string' version of the entity name to avoid circular dependency issues. - @ManyToMany(() => 'User', (user: User) => user.files_visibility_groups, { owner: true, nullable: true }) + @ManyToMany(() => 'User', (user: User) => user.files_visibility_groups, { + owner: true, + nullable: true, + serializedName: 'users_count', + serializer: (u: User[]) => u.length, + }) users = new Collection(this); - @OneToMany(() => File, (file) => file.visibility, { nullable: true }) + @OneToMany(() => File, (file) => file.visibility, { + nullable: true, + serializedName: 'files_count', + serializer: (f: File[]) => f.length, + }) files = new Collection>(this); } diff --git a/src/modules/files/entities/file.entity.ts b/src/modules/files/entities/file.entity.ts index 1f251f73..754b1047 100644 --- a/src/modules/files/entities/file.entity.ts +++ b/src/modules/files/entities/file.entity.ts @@ -31,7 +31,12 @@ export abstract class File extends BaseEntity { @Property() size: number; - @ManyToOne(() => FileVisibilityGroup, { nullable: true, default: null }) + @ManyToOne(() => FileVisibilityGroup, { + nullable: true, + default: null, + serializedName: 'visibility_id', + serializer: (v: FileVisibilityGroup) => v?.id, + }) visibility?: FileVisibilityGroup; @Property({ nullable: true, default: null }) diff --git a/src/modules/logs/dto/get.dto.ts b/src/modules/logs/dto/get.dto.ts index 8a386083..54123fc8 100644 --- a/src/modules/logs/dto/get.dto.ts +++ b/src/modules/logs/dto/get.dto.ts @@ -6,7 +6,7 @@ import { IsInt, IsString } from 'class-validator'; export class LogDTO implements ILogDTO { @ApiProperty() @IsInt() - user: number; + user_id: number; @ApiProperty() @IsString() diff --git a/src/modules/logs/entities/log.entity.ts b/src/modules/logs/entities/log.entity.ts index 0b68d4e1..6b8dc042 100644 --- a/src/modules/logs/entities/log.entity.ts +++ b/src/modules/logs/entities/log.entity.ts @@ -5,7 +5,7 @@ import { User } from '@modules/users/entities/user.entity'; @Entity({ tableName: 'users_logs' }) export class Log extends BaseEntity { - @ManyToOne() + @ManyToOne(() => User, { serializedName: 'user_id', serializer: (u: User) => u.id }) user: User; @Property() diff --git a/src/modules/logs/logs.service.ts b/src/modules/logs/logs.service.ts index fb36ed1f..e7dbafa3 100644 --- a/src/modules/logs/logs.service.ts +++ b/src/modules/logs/logs.service.ts @@ -21,7 +21,7 @@ export class LogsService { } async getUserLogs(id: number): Promise { - return (await this.orm.em.find(Log, { user: id })).map((log) => ({ ...log, user: log.user.id })); + return (await this.orm.em.find(Log, { user: id })).map((log) => log.toObject() as unknown as LogDTO); } async deleteUserLogs(id: number) { diff --git a/src/modules/permissions/dto/get.dto.ts b/src/modules/permissions/dto/get.dto.ts index a7c201d4..f1010119 100644 --- a/src/modules/permissions/dto/get.dto.ts +++ b/src/modules/permissions/dto/get.dto.ts @@ -23,7 +23,7 @@ export class PermissionGetDTO extends BaseResponseDTO implements IPermissionGetD @ApiProperty() @IsInt() - user: number; + user_id: number; @ApiProperty() @IsBoolean() diff --git a/src/modules/permissions/entities/permission.entity.ts b/src/modules/permissions/entities/permission.entity.ts index 924cfdc5..cc5fd373 100644 --- a/src/modules/permissions/entities/permission.entity.ts +++ b/src/modules/permissions/entities/permission.entity.ts @@ -16,6 +16,11 @@ export class Permission extends BaseEntity { @Property({ name: 'expires_at' }) expires: Date; - @ManyToOne(() => User, { onDelete: 'cascade', joinColumn: 'user_id' }) + @ManyToOne(() => User, { + onDelete: 'cascade', + joinColumn: 'user_id', + serializedName: 'user_id', + serializer: (u: User) => u.id, + }) user: User; } diff --git a/src/modules/permissions/permissions.service.ts b/src/modules/permissions/permissions.service.ts index cd575227..aa1bab25 100644 --- a/src/modules/permissions/permissions.service.ts +++ b/src/modules/permissions/permissions.service.ts @@ -68,7 +68,7 @@ export class PermissionsService { // Save it & return it await this.orm.em.persistAndFlush(permission); - return { ...permission, user: user.id } as PermissionGetDTO; + return permission.toObject() as unknown as PermissionGetDTO; } /** @@ -82,7 +82,7 @@ export class PermissionsService { if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); const permissions = await user.permissions.loadItems(); - return permissions.map((p) => ({ ...p, user: user.id })); + return permissions.map((p) => p.toObject() as unknown as PermissionGetDTO); } @CreateRequestContext() @@ -98,6 +98,6 @@ export class PermissionsService { if (data.revoked !== undefined) perm.revoked = data.revoked; await this.orm.em.persistAndFlush(perm); - return { ...perm, user: user.id }; + return perm.toObject() as unknown as PermissionGetDTO; } } diff --git a/src/modules/promotions/dto/get.dto.ts b/src/modules/promotions/dto/get.dto.ts index 111cbbd4..e193d84d 100644 --- a/src/modules/promotions/dto/get.dto.ts +++ b/src/modules/promotions/dto/get.dto.ts @@ -13,7 +13,7 @@ export class PromotionResponseDTO extends BaseResponseDTO implements IPromotionR @ApiProperty() @IsInt() - users: number; + users_count: number; @ApiProperty({ required: false }) @IsInt() @@ -23,5 +23,5 @@ export class PromotionResponseDTO extends BaseResponseDTO implements IPromotionR export class PromotionPictureResponseDTO extends FileGetDTO implements IPromotionPictureResponseDTO { @ApiProperty() @IsInt() - picture_promotion: number; + picture_promotion_id: number; } diff --git a/src/modules/promotions/entities/promotion-picture.entity.ts b/src/modules/promotions/entities/promotion-picture.entity.ts index dc1035f5..7c968aef 100644 --- a/src/modules/promotions/entities/promotion-picture.entity.ts +++ b/src/modules/promotions/entities/promotion-picture.entity.ts @@ -6,10 +6,15 @@ import { Promotion } from './promotion.entity'; @Entity() export class PromotionPicture extends File { - @OneToOne(() => Promotion, (promotion) => promotion.picture, { nullable: true, owner: true }) + @OneToOne(() => Promotion, (promotion) => promotion.picture, { + nullable: true, + owner: true, + serializedName: 'picture_promotion_id', + serializer: (p: Promotion) => p.id, + }) picture_promotion: Promotion; - @Property({ persist: false }) + @Property({ persist: false, hidden: true }) get owner(): Promotion { return this.picture_promotion; } diff --git a/src/modules/promotions/entities/promotion.entity.ts b/src/modules/promotions/entities/promotion.entity.ts index f66cad71..0e15f332 100644 --- a/src/modules/promotions/entities/promotion.entity.ts +++ b/src/modules/promotions/entities/promotion.entity.ts @@ -10,9 +10,18 @@ export class Promotion extends BaseEntity implements Promotion { @Property() number: number; - @OneToMany(() => User, (user) => user.promotion, { cascade: [Cascade.REMOVE] }) + @OneToMany(() => User, (user) => user.promotion, { + cascade: [Cascade.REMOVE], + serializedName: 'users_count', + serializer: (u: User[]) => u.length, + }) users: Collection; - @OneToOne(() => PromotionPicture, (picture) => picture.picture_promotion, { cascade: [Cascade.ALL], nullable: true }) + @OneToOne(() => PromotionPicture, (picture) => picture.picture_promotion, { + cascade: [Cascade.ALL], + nullable: true, + serializedName: 'picture_id', + serializer: (p: PromotionPicture) => p?.id, + }) picture?: PromotionPicture; } diff --git a/src/modules/promotions/promotions.service.ts b/src/modules/promotions/promotions.service.ts index de9bef6e..71a312c1 100644 --- a/src/modules/promotions/promotions.service.ts +++ b/src/modules/promotions/promotions.service.ts @@ -36,43 +36,32 @@ export class PromotionsService { @CreateRequestContext() async findAll(): Promise { - const promotions = await this.orm.em.find(Promotion, {}, { fields: ['*', 'picture', 'users'] }); - const res: PromotionResponseDTO[] = []; - - for (const promotion of promotions) { - res.push({ ...promotion, users: promotion.users.count(), picture: promotion.picture?.id }); - } - - return res; + return (await this.orm.em.find(Promotion, {}, { fields: ['*', 'users'] })).map( + (p) => p.toObject() as unknown as PromotionResponseDTO, + ); } @CreateRequestContext() async findLatest(): Promise { const promotion = ( - await this.orm.em.find(Promotion, {}, { orderBy: { number: 'DESC' }, fields: ['*', 'picture', 'users'] }) + await this.orm.em.find( + Promotion, + {}, + { orderBy: { number: 'DESC' }, limit: 1, fields: ['*', 'picture', 'users'] }, + ) )[0]; - - return { - ...promotion, - users: promotion.users.count(), - picture: promotion.picture?.id, - }; + return promotion.toObject() as unknown as PromotionResponseDTO; } @CreateRequestContext() async findCurrent(): Promise { - const promotions = await this.orm.em.find( - Promotion, - {}, - { orderBy: { number: 'DESC' }, fields: ['*', 'picture', 'users'], limit: 5 }, - ); - const res: PromotionResponseDTO[] = []; - - for (const promotion of promotions) { - res.push({ ...promotion, users: promotion.users.count(), picture: promotion.picture?.id }); - } - - return res; + return ( + await this.orm.em.find( + Promotion, + {}, + { orderBy: { number: 'DESC' }, limit: 5, fields: ['*', 'picture', 'users'] }, + ) + ).map((p) => p.toObject() as unknown as PromotionResponseDTO); } @CreateRequestContext() @@ -80,11 +69,7 @@ export class PromotionsService { const promotion = await this.orm.em.findOne(Promotion, { number }, { fields: ['*', 'picture', 'users'] }); if (!promotion) throw new NotFoundException(this.t.Errors.Id.NotFound(Promotion, number)); - return { - ...promotion, - users: promotion.users.count(), - picture: promotion.picture?.id, - }; + return promotion.toObject() as unknown as PromotionResponseDTO; } @CreateRequestContext() @@ -139,8 +124,7 @@ export class PromotionsService { }); await this.orm.em.persistAndFlush(promotion); - - return { ...promotion.picture, picture_promotion: promotion.number, visibility: promotion.picture.visibility?.id }; + return promotion.picture.toObject() as unknown as PromotionPictureResponseDTO; } @CreateRequestContext() diff --git a/src/modules/roles/dto/get.dto.ts b/src/modules/roles/dto/get.dto.ts index b1d23634..70d2b970 100644 --- a/src/modules/roles/dto/get.dto.ts +++ b/src/modules/roles/dto/get.dto.ts @@ -27,5 +27,5 @@ export class RoleGetDTO implements IRoleGetDTO { @ApiProperty({ type: Number, default: 1 }) @IsInt() - users: number; + users_count: number; } diff --git a/src/modules/roles/entities/role-expiration.entity.ts b/src/modules/roles/entities/role-expiration.entity.ts index 4bf23feb..f9b0261d 100644 --- a/src/modules/roles/entities/role-expiration.entity.ts +++ b/src/modules/roles/entities/role-expiration.entity.ts @@ -8,10 +8,10 @@ import { Role } from './role.entity'; @Entity({ tableName: 'roles_expirations' }) export class RoleExpiration extends BaseEntity { - @ManyToOne(() => User) + @ManyToOne(() => User, { serializedName: 'user_id', serializer: (u: User) => u.id }) user: User; - @ManyToOne(() => Role) + @ManyToOne(() => Role, { serializedName: 'role_id', serializer: (r: Role) => r.id }) role: Role; @Property({ name: 'expires_at' }) diff --git a/src/modules/roles/entities/role.entity.ts b/src/modules/roles/entities/role.entity.ts index 65c12d39..6ed4a4dd 100644 --- a/src/modules/roles/entities/role.entity.ts +++ b/src/modules/roles/entities/role.entity.ts @@ -19,6 +19,10 @@ export class Role extends BaseEntity { @Property({ name: 'permissions' }) permissions: PERMISSION_NAMES[]; - @ManyToMany(() => User, (user) => user.roles, { owner: true }) + @ManyToMany(() => User, (user) => user.roles, { + owner: true, + serializedName: 'users_count', + serializer: (u: User[]) => u.length, + }) users = new Collection(this); } diff --git a/src/modules/roles/roles.service.ts b/src/modules/roles/roles.service.ts index 2a958c01..2b4ca751 100644 --- a/src/modules/roles/roles.service.ts +++ b/src/modules/roles/roles.service.ts @@ -49,7 +49,7 @@ export class RolesService { @CreateRequestContext() async getAllRoles(): Promise { const roles = await this.orm.em.find(Role, {}, { populate: ['users'] }); - return roles.map((r) => ({ ...r, users: r.users.count() })); + return roles.map((r) => r.toObject() as unknown as RoleGetDTO); } @CreateRequestContext() @@ -57,7 +57,7 @@ export class RolesService { const role = await this.orm.em.findOne(Role, { id }, { populate: ['users'] }); if (!role) throw new NotFoundException(this.t.Errors.Id.NotFound(Role, id)); - return { ...role, users: role.users.count() }; + return role.toObject() as unknown as RoleGetDTO; } @CreateRequestContext() @@ -77,8 +77,7 @@ export class RolesService { const role = this.orm.em.create(Role, { name: roleName, permissions }); await this.orm.em.persistAndFlush(role); - delete role.users; - return { ...role, users: 0 }; + return role.toObject() as unknown as RoleGetDTO; } @CreateRequestContext() @@ -90,7 +89,7 @@ export class RolesService { role.permissions = input.permissions.unique(); await this.orm.em.persistAndFlush(role); - return { ...role, users: role.users.count() }; + return role.toObject() as unknown as RoleGetDTO; } @CreateRequestContext() diff --git a/src/modules/translate/translate.service.ts b/src/modules/translate/translate.service.ts index 5a442d07..01405737 100644 --- a/src/modules/translate/translate.service.ts +++ b/src/modules/translate/translate.service.ts @@ -111,5 +111,8 @@ export class TranslateService { Email: { Verified: (email: email) => this.generic('responses.success.email.verified', { email }), }, + User: { + Registered: () => this.generic('responses.success.user.register', {}), + }, }; } diff --git a/src/modules/users/controllers/users-data.controller.ts b/src/modules/users/controllers/users-data.controller.ts index a248e16d..e4a5616f 100644 --- a/src/modules/users/controllers/users-data.controller.ts +++ b/src/modules/users/controllers/users-data.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Req, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { z } from 'zod'; import { USER_GENDER } from '@exported/api/constants/genders'; @@ -38,6 +38,7 @@ export class UsersDataController { @ApiOperation({ summary: 'Creates new users' }) @ApiOkResponse({ description: 'The created user', type: [BaseUserResponseDTO] }) @ApiNotOkResponses({ 400: 'Invalid input', 401: 'Insufficient permission' }) + @ApiBody({ type: [CreateUserDTO] }) async create(@Body() input: CreateUserDTO[]): Promise { const schema = z .object({ @@ -108,7 +109,7 @@ export class UsersDataController { async getPrivate(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - return this.usersService.findOneAsDTO(id, false); + return await this.usersService.findOne(id, false); } @Get(':id/data/public') @@ -121,7 +122,7 @@ export class UsersDataController { async getPublic(@Param('id') id: number): Promise { validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - return this.usersService.findOneAsDTO(id); + return this.usersService.findOne(id); } @Get(':id/data/visibility') diff --git a/src/modules/users/dto/get.dto.ts b/src/modules/users/dto/get.dto.ts index aac9d5a5..2970a687 100644 --- a/src/modules/users/dto/get.dto.ts +++ b/src/modules/users/dto/get.dto.ts @@ -1,6 +1,7 @@ import type { email } from '#types'; import type { IUserBannerResponseDTO, + IUserGetPrivateDTO, IUserPictureResponseDTO, IUserRoleGetDTO, IUserVisibilityGetDTO, @@ -10,7 +11,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsBoolean, IsDate, IsEmail, IsIn, IsInt, IsNumber, IsString } from 'class-validator'; import { IUserGetDTO, PERMISSION_NAMES } from '#types/api'; -import { USER_GENDER } from '@exported/api/constants/genders'; +import { USER_GENDER, genders } from '@exported/api/constants/genders'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; import { BaseResponseDTO } from '@modules/_mixin/dto/base.dto'; import { FileGetDTO } from '@modules/files/dto/get.dto'; @@ -54,6 +55,10 @@ export class UserGetDTO extends BaseResponseDTO implements IUserGetDTO { @IsString() last_name: string; + @ApiProperty({ example: 'John Doe' }) + @IsString() + full_name: string; + @ApiProperty({ minimum: 1 }) @IsNumber() picture?: number; @@ -64,15 +69,23 @@ export class UserGetDTO extends BaseResponseDTO implements IUserGetDTO { @ApiProperty({ type: String, example: 'example@domain.com' }) @IsEmail() - email: email; + email?: email; @ApiProperty({ type: Boolean, default: false }) @IsBoolean() - email_verified: boolean; + email_verified?: boolean; @ApiProperty({ example: new Date('1999-12-31').toISOString() }) @IsDate() - birth_date: Date; + birth_date?: Date; + + @ApiProperty({ example: 21 }) + @IsNumber() + age: number; + + @ApiProperty() + @IsBoolean() + is_minor: boolean; @ApiProperty() @IsString() @@ -81,7 +94,7 @@ export class UserGetDTO extends BaseResponseDTO implements IUserGetDTO { @ApiProperty({ example: USER_GENDER[0], enum: USER_GENDER }) @IsString() @IsIn(USER_GENDER) - gender?: (typeof USER_GENDER)[number]; + gender?: genders; @ApiProperty({ example: null }) @IsString() @@ -116,10 +129,93 @@ export class UserGetDTO extends BaseResponseDTO implements IUserGetDTO { verified?: Date; } +export class UserGetPrivateDTO extends BaseResponseDTO implements IUserGetPrivateDTO { + @ApiProperty({ example: 'John' }) + @IsString() + first_name: string; + + @ApiProperty({ example: 'Doe' }) + @IsString() + last_name: string; + + @ApiProperty({ example: 'John Doe' }) + @IsString() + full_name: string; + + @ApiProperty({ minimum: 1 }) + @IsNumber() + picture?: number; + + @ApiProperty({ minimum: 1 }) + @IsNumber() + banner?: number; + + @ApiProperty({ type: String, example: 'example@domain.com' }) + @IsEmail() + email: email; + + @ApiProperty({ type: Boolean, default: false }) + @IsBoolean() + email_verified?: boolean; + + @ApiProperty({ example: new Date('1999-12-31').toISOString() }) + @IsDate() + birth_date: Date; + + @ApiProperty({ example: 21 }) + @IsNumber() + age: number; + + @ApiProperty() + @IsBoolean() + is_minor: boolean; + + @ApiProperty() + @IsString() + nickname?: string; + + @ApiProperty({ example: USER_GENDER[0], enum: USER_GENDER }) + @IsString() + @IsIn(USER_GENDER) + gender: genders; + + @ApiProperty({ example: null }) + @IsString() + pronouns: string; + + @ApiProperty({ type: Number, minimum: 1 }) + @IsNumber() + promotion: number; + + @ApiProperty({ example: new Date().toISOString() }) + @IsDate() + last_seen?: Date; + + @ApiProperty({ example: false }) + @IsBoolean() + subscribed: boolean; // TODO: (KEY: 2) Make a PR to implement subscriptions in the API + + @ApiProperty() + @IsEmail() + secondary_email: string; + + @ApiProperty() + @IsString() + phone: string; + + @ApiProperty() + @IsString() + parent_contact: string; + + @ApiProperty({ type: Date }) + @IsDate() + verified?: Date; +} + export class UserVisibilityGetDTO implements IUserVisibilityGetDTO { @ApiProperty({ minimum: 1 }) @IsInt() - user: number; + user_id: number; @ApiProperty({ type: Boolean, default: false }) @IsBoolean() @@ -157,11 +253,11 @@ export class UserVisibilityGetDTO implements IUserVisibilityGetDTO { export class UserGetPictureDTO extends FileGetDTO implements IUserPictureResponseDTO { @ApiProperty({ minimum: 1 }) @IsInt() - picture_user: number; + picture_user_id: number; } export class UserGetBannerDTO extends FileGetDTO implements IUserBannerResponseDTO { @ApiProperty({ minimum: 1 }) @IsInt() - banner_user: number; + banner_user_id: number; } diff --git a/src/modules/users/entities/user-banner.entity.ts b/src/modules/users/entities/user-banner.entity.ts index 318f8928..f6331c7e 100644 --- a/src/modules/users/entities/user-banner.entity.ts +++ b/src/modules/users/entities/user-banner.entity.ts @@ -6,10 +6,15 @@ import { User } from './user.entity'; @Entity() export class UserBanner extends File { - @OneToOne(() => User, (user) => user.banner, { nullable: true, owner: true }) + @OneToOne(() => User, (user) => user.banner, { + nullable: true, + owner: true, + serializedName: 'banner_user_id', + serializer: (u: User) => u.id, + }) banner_user: User; - @Property({ persist: false }) + @Property({ persist: false, hidden: true }) get owner(): User { return this.banner_user; } diff --git a/src/modules/users/entities/user-picture.entity.ts b/src/modules/users/entities/user-picture.entity.ts index 86349300..d479ebde 100644 --- a/src/modules/users/entities/user-picture.entity.ts +++ b/src/modules/users/entities/user-picture.entity.ts @@ -6,10 +6,15 @@ import { User } from './user.entity'; @Entity() export class UserPicture extends File { - @OneToOne(() => User, (user) => user.picture, { nullable: true, owner: true }) + @OneToOne(() => User, (user) => user.picture, { + nullable: true, + owner: true, + serializedName: 'picture_user_id', + serializer: (u: User) => u.id, + }) picture_user: User; - @Property({ persist: false }) + @Property({ persist: false, hidden: true }) get owner(): User { return this.picture_user; } diff --git a/src/modules/users/entities/user-visibility.entity.ts b/src/modules/users/entities/user-visibility.entity.ts index 4d6a4ebb..dfe0dfa5 100644 --- a/src/modules/users/entities/user-visibility.entity.ts +++ b/src/modules/users/entities/user-visibility.entity.ts @@ -7,7 +7,12 @@ import { User } from './user.entity'; @Entity({ tableName: 'users_visibility' }) export class UserVisibility extends BaseEntity { /** Specify to which user those parameters belongs */ - @OneToOne(() => User, { onDelete: 'cascade', joinColumn: 'user_id' }) + @OneToOne(() => User, { + onDelete: 'cascade', + joinColumn: 'user_id', + serializedName: 'user_id', + serializer: (u: User) => u.id, + }) user: User; /** Wether the user email should be visible or not */ diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index f065a159..398b3984 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -18,7 +18,7 @@ import { UserVisibility } from './user-visibility.entity'; export type UserPrivateKeys = Omit; export type UserPrivate = User; -export type UserPublic = Omit & Pick; +export type UserPublic = Omit & Partial>; export type Request = Express.Request & { user: User; @@ -39,16 +39,26 @@ export class User extends BaseEntity { return `${this.first_name} ${this.last_name}`; } - @OneToOne(() => UserPicture, (picture) => picture.picture_user, { cascade: [Cascade.ALL], nullable: true }) + @OneToOne(() => UserPicture, (picture) => picture.picture_user, { + cascade: [Cascade.ALL], + nullable: true, + serializedName: 'picture_id', + serializer: (p: UserPicture) => p?.id, + }) picture?: UserPicture; - @OneToOne(() => UserBanner, (banner) => banner.banner_user, { cascade: [Cascade.ALL], nullable: true }) + @OneToOne(() => UserBanner, (banner) => banner.banner_user, { + cascade: [Cascade.ALL], + nullable: true, + serializedName: 'banner_id', + serializer: (b: UserBanner) => b?.id, + }) banner?: UserBanner; @Property({ unique: true, type: String }) email: email; - @Property({ onCreate: () => false }) + @Property({ onCreate: () => false, hidden: true }) email_verified: boolean; @Property({ nullable: true, hidden: true }) @@ -116,19 +126,25 @@ export class User extends BaseEntity { cascade: [Cascade.REMOVE], orphanRemoval: true, nullable: true, + hidden: true, }) permissions? = new Collection(this); - @ManyToMany(() => Role, (role) => role.users, { nullable: true }) + @ManyToMany(() => Role, (role) => role.users, { nullable: true, hidden: true }) roles? = new Collection(this); - @OneToMany(() => Log, (log) => log.user, { cascade: [Cascade.REMOVE], orphanRemoval: true, nullable: true }) + @OneToMany(() => Log, (log) => log.user, { + cascade: [Cascade.REMOVE], + orphanRemoval: true, + nullable: true, + hidden: true, + }) logs? = new Collection(this); - @Property({ type: Date, nullable: true, onCreate: () => null }) + @Property({ type: Date, nullable: true, onCreate: () => null, hidden: true }) verified?: Date; //* FILES - @ManyToMany(() => FileVisibilityGroup, (group) => group.users, { nullable: true }) + @ManyToMany(() => FileVisibilityGroup, (group) => group.users, { nullable: true, hidden: true }) files_visibility_groups? = new Collection(this); } diff --git a/src/modules/users/services/users-data.service.ts b/src/modules/users/services/users-data.service.ts index d14b73fb..39c18b3e 100644 --- a/src/modules/users/services/users-data.service.ts +++ b/src/modules/users/services/users-data.service.ts @@ -1,4 +1,4 @@ -import type { KeysOf, email } from '#types'; +import type { email } from '#types'; import type { I18nTranslations, PERMISSION_NAMES } from '#types/api'; import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; @@ -16,13 +16,13 @@ import { PermissionGetDTO } from '@modules/permissions/dto/get.dto'; import { RoleExpiration } from '@modules/roles/entities/role-expiration.entity'; import { TranslateService } from '@modules/translate/translate.service'; import { UserVisibility } from '@modules/users/entities/user-visibility.entity'; -import { User, UserPrivate, UserPublic } from '@modules/users/entities/user.entity'; +import { User, UserPrivate } from '@modules/users/entities/user.entity'; import { checkBirthDate } from '@utils/dates'; import { checkPasswordStrength, generateRandomPassword } from '@utils/password'; import { getTemplate } from '@utils/template'; import { BaseUserResponseDTO } from '../dto/base-user.dto'; -import { UserGetDTO, UserRoleGetDTO, UserVisibilityGetDTO } from '../dto/get.dto'; +import { UserGetDTO, UserGetPrivateDTO, UserRoleGetDTO, UserVisibilityGetDTO } from '../dto/get.dto'; import { UserPatchDTO, UserVisibilityPatchDTO } from '../dto/patch.dto'; @Injectable() @@ -58,25 +58,16 @@ export class UsersDataService { * @returns {Promise} The filtered users */ @CreateRequestContext() - async removePrivateFields(users: User[]): Promise { - const res: UserPublic[] = []; + async sanitize(users: User[]): Promise { + const res: UserGetDTO[] = []; const visibilities = await this.findVisibilities(users.map((u) => u.id)); visibilities.forEach((v) => { - const user = users.find((u) => u.id === v.user); + const user = users.find((u) => u.id === v.user_id).toObject() as unknown as UserGetDTO; - Object.entries(v).forEach(([key, value]) => { - // FIXME: Element implicitly has an 'any' type because expression of type 'string' - // can't be used to index type 'User'. - // -> one solution is to use user[key as keyof User], but does not work because of - // the getters of User - // @ts-ignore - if (value === false) user[key] = undefined; - }); - - const privateFields: KeysOf = ['files_visibility_groups', 'logs', 'roles', 'permissions']; - privateFields.forEach((key) => { - if (user[key]) delete user[key]; + Object.entries(v).forEach(([key, val]) => { + const key_ = key as keyof Omit; + if (val === false) delete user[key_]; }); res.push(user); @@ -111,21 +102,21 @@ export class UsersDataService { * @param {Partial} id_or_email The id or email of the user to find * @param {boolean} filter Whether to filter the user or not (default: true) * - * @returns {Promise} The user found (public if filter is true) + * @returns {Promise} The user found (public if filter is true) * @throws {BadRequestException} If no id or email is provided * @throws {NotFoundException} If no user is found with the provided id/email * * @example * ```ts - * const user1: UserPrivate = await this.usersService.findOne({ id: 1 }, false); - * const user2: UserPublic = await this.usersService.findOne({ email: 'example@domain.com' }); + * const user1: UserGetPrivateDTO = await this.usersService.findOne({ id: 1 }, false); + * const user2: UserGetDTO = await this.usersService.findOne({ email: 'example@domain.com' }); * ``` */ - async findOne(id_or_email: number | email, filter: false): Promise; - async findOne(id_or_email: number | email): Promise; + async findOne(id_or_email: number | email, filter: false): Promise; + async findOne(id_or_email: number | email): Promise; @CreateRequestContext() - async findOne(id_or_email: number | email, filter = true): Promise { + async findOne(id_or_email: number | email, filter = true): Promise { let user: User = null; const parsed = z.union([z.coerce.number(), z.string().email()]).parse(id_or_email); @@ -136,12 +127,7 @@ export class UsersDataService { if (!user && typeof parsed === 'string') throw new NotFoundException(this.t.Errors.Email.NotFound(User, parsed as email)); - return filter ? (await this.removePrivateFields([user]))[0] : user; - } - - async findOneAsDTO(id_or_email: number | email, filter: boolean = true): Promise { - const user = await this.findOne(id_or_email, filter as false); - return { ...user, picture: user.picture?.id, banner: user.banner?.id, promotion: user.promotion?.id }; + return filter ? (await this.sanitize([user]))[0] : user; } /** @@ -157,7 +143,7 @@ export class UsersDataService { if (!users || users.length === 0) throw new NotFoundException(this.t.Errors.Id.NotFounds(User, ids)); const visibilities = await this.orm.em.find(UserVisibility, { user: { $in: users } }); - return visibilities.map((v) => ({ ...v, user: v.user.id })); + return visibilities.map((v) => v.toObject() as unknown as UserVisibilityGetDTO); } @CreateRequestContext() @@ -173,7 +159,7 @@ export class UsersDataService { Object.assign(visibility, input); await this.orm.em.persistAndFlush(visibility); - return { ...visibility, user: visibility.user.id }; + return visibility.toObject() as unknown as UserVisibilityGetDTO; } @CreateRequestContext() @@ -206,7 +192,7 @@ export class UsersDataService { // Save changes to the database & create the user's visibility parameters this.orm.em.create(UserVisibility, { user }); - return { message: 'this.t.Success.User.Register(registerDto.email)', statusCode: 200 }; + return { message: this.t.Success.User.Registered(), statusCode: 201 }; } @CreateRequestContext() @@ -214,7 +200,7 @@ export class UsersDataService { this.emailsService.validateEmail(email); // Add the email verification token & create the user - const user = await this.findOne(id, false); + const user = await this.orm.em.findOne(User, { id }); const user_b = await this.orm.em.findOne(User, { email }); // Check if the email is already used by someone else @@ -293,8 +279,8 @@ export class UsersDataService { } @CreateRequestContext() - async verifyEmail(user_id: number, token: string): Promise { - const user = await this.findOne(user_id, false); + async verifyEmail(id: number, token: string): Promise { + const user = await this.orm.em.findOne(User, { id }); if (user.email_verified) throw new BadRequestException(this.t.Errors.Email.AlreadyVerified(User)); if (!compareSync(token, user.email_verification)) @@ -316,7 +302,8 @@ export class UsersDataService { const users: User[] = []; for (const input of inputs) { - const user = await this.findOne(input.id, false); + const user = await this.orm.em.findOne(User, { id: input.id }); + if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, input.id)); if (input.email) await this.updateUserEmail(user.id, input.email); @@ -337,7 +324,7 @@ export class UsersDataService { users.push(user); } - return users.map((u) => ({ ...u, picture: u.picture?.id, banner: u.banner?.id, promotion: u.promotion?.id })); + return this.sanitize(users); } @CreateRequestContext() @@ -386,7 +373,7 @@ export class UsersDataService { if (!input.show_expired) permissions.filter((p) => p.expires > new Date()); if (!input.show_revoked) permissions.filter((p) => p.revoked === false); - return permissions.map((p) => ({ ...p, user: id })); + return permissions.map((p) => p.toObject() as unknown as PermissionGetDTO); } /** diff --git a/src/modules/users/services/users-files.service.ts b/src/modules/users/services/users-files.service.ts index 4dd0730a..9f11ad4f 100644 --- a/src/modules/users/services/users-files.service.ts +++ b/src/modules/users/services/users-files.service.ts @@ -79,11 +79,7 @@ export class UsersFilesService { }); await this.orm.em.persistAndFlush(user); - - delete user.picture.picture_user; // avoid circular reference - delete user.picture.visibility; - - return { ...user.picture, picture_user: user.id, visibility: user.picture.visibility.id }; + return user.picture.toObject() as unknown as UserGetPictureDTO; } @CreateRequestContext() @@ -104,7 +100,7 @@ export class UsersFilesService { this.filesService.deleteFromDisk(user.picture); await this.orm.em.removeAndFlush(user.picture); - return { message: this.t.Success.Entity.Deleted(UserPicture), statusCode: 200 }; + return { message: this.t.Success.Entity.Deleted(UserPicture), statusCode: 201 }; } @CreateRequestContext() @@ -141,10 +137,7 @@ export class UsersFilesService { }); await this.orm.em.persistAndFlush(user); - - delete user.banner.banner_user; // avoid circular reference - delete user.banner.visibility; - return { ...user.banner, banner_user: user.id, visibility: user.banner.visibility.id }; + return user.banner.toObject() as unknown as UserGetBannerDTO; } @CreateRequestContext() @@ -165,6 +158,6 @@ export class UsersFilesService { this.filesService.deleteFromDisk(user.banner); await this.orm.em.removeAndFlush(user.banner); - return { message: this.t.Success.Entity.Deleted(UserBanner), statusCode: 200 }; + return { message: this.t.Success.Entity.Deleted(UserBanner), statusCode: 201 }; } } diff --git a/tests/e2e/auth.e2e-spec.ts b/tests/e2e/auth.e2e-spec.ts index c77f97f7..ffdb44d1 100644 --- a/tests/e2e/auth.e2e-spec.ts +++ b/tests/e2e/auth.e2e-spec.ts @@ -3,7 +3,6 @@ import type { email } from '#types'; import { hashSync } from 'bcrypt'; import request from 'supertest'; -import { USER_GENDER } from '@exported/api/constants/genders'; import { UserPostDTO } from '@modules/auth/dto/post.dto'; import { User } from '@modules/users/entities/user.entity'; import { generateRandomPassword } from '@utils/password'; @@ -221,28 +220,8 @@ describe('Auth (e2e)', () => { .expect(201); expect(response.body).toEqual({ - age: (() => { - const diff = Date.now() - user.birth_date.getTime(); - const age = new Date(diff); - return Math.abs(age.getUTCFullYear() - 1970); - })(), - birth_date: '2000-01-01T00:00:00.000Z', - created: expect.any(String), - last_seen: expect.any(String), - email: 'johndoe@domain.com', - email_verified: false, - files_visibility_groups: [], - first_name: 'John', - full_name: 'John Doe', - id: expect.any(Number), - gender: USER_GENDER[0], - is_minor: false, - last_name: 'Doe', - logs: [], - permissions: [], - roles: [], - updated: expect.any(String), - verified: null, + message: t.Success.User.Registered(), + statusCode: 201, }); }); }); @@ -305,7 +284,7 @@ describe('Auth (e2e)', () => { expect(response.body).toEqual({ message: t.Success.Email.Verified('unverified@email.com'), - status_code: 200, + statusCode: 200, }); // Reset user email_verified to false (for other tests) diff --git a/tests/e2e/logs.e2e-spec.ts b/tests/e2e/logs.e2e-spec.ts index b71a188a..d4f74b29 100644 --- a/tests/e2e/logs.e2e-spec.ts +++ b/tests/e2e/logs.e2e-spec.ts @@ -101,23 +101,6 @@ describe('Logs (e2e)', () => { }); }); - describe('404 : Not Found', () => { - it('when the user does not exist', async () => { - const fakeId = 9999; - - const response = await request(server) - .get(`/logs/user/${fakeId}`) - .set('Authorization', `Bearer ${tokenLogModerator}`) - .expect(404); - - expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Id.NotFound(User, fakeId), - }); - }); - }); - describe('200 : Ok', () => { it('when user is asking for himself', async () => { const response = await request(server) @@ -150,7 +133,7 @@ describe('Logs (e2e)', () => { id: expect.any(Number), created: expect.any(String), updated: expect.any(String), - user: userIdUnauthorized, + user_id: userIdUnauthorized, action: expect.any(String), ip: expect.any(String), user_agent: expect.any(String), diff --git a/tests/e2e/permissions.e2e-spec.ts b/tests/e2e/permissions.e2e-spec.ts index cdeaf574..1da77a31 100644 --- a/tests/e2e/permissions.e2e-spec.ts +++ b/tests/e2e/permissions.e2e-spec.ts @@ -137,7 +137,7 @@ describe('Permissions (e2e)', () => { name: 'CAN_EDIT_PROMOTION', revoked: false, updated: expect.any(String), - user: 1, + user_id: 1, }); // Remove the permission after the test @@ -221,7 +221,7 @@ describe('Permissions (e2e)', () => { name: 'ROOT', revoked: false, updated: expect.any(String), - user: 1, + user_id: 1, }); }); }); @@ -299,7 +299,7 @@ describe('Permissions (e2e)', () => { name: 'ROOT', revoked: false, updated: expect.any(String), - user: 1, + user_id: 1, }, ]); }); diff --git a/tests/e2e/promotions.e2e-spec.ts b/tests/e2e/promotions.e2e-spec.ts index 29a1bc9b..b87f09fe 100644 --- a/tests/e2e/promotions.e2e-spec.ts +++ b/tests/e2e/promotions.e2e-spec.ts @@ -80,8 +80,7 @@ describe('Promotions (e2e)', () => { created: expect.any(String), updated: expect.any(String), number: 1, - picture: null, - users: 0, + users_count: 0, }); }); }); @@ -130,8 +129,7 @@ describe('Promotions (e2e)', () => { created: expect.any(String), updated: expect.any(String), number: expect.any(Number), - picture: null, - users: expect.any(Number), + users_count: expect.any(Number), }); }); }); @@ -174,8 +172,7 @@ describe('Promotions (e2e)', () => { created: expect.any(String), updated: expect.any(String), number: expect.any(Number), - picture: null, - users: expect.any(Number), + users_count: expect.any(Number), }); }); }); @@ -246,8 +243,7 @@ describe('Promotions (e2e)', () => { created: expect.any(String), updated: expect.any(String), number: 21, - picture: null, - users: 1, + users_count: 1, }); }); }); @@ -521,33 +517,28 @@ describe('Promotions (e2e)', () => { }); }); - describe('200 : Ok', () => { + describe('201 : Created', () => { it('when the promotion exists and set the logo', async () => { const response = await request(server) .post('/promotions/21/logo') .set('Authorization', `Bearer ${tokenPromotionModerator}`) - .attach('file', filePictureSquare, 'file.png'); + .attach('file', filePictureSquare, 'file.png') + .expect(201); // expect registered data to be returned expect(response.body).toEqual({ - id: 21, + id: expect.any(Number), created: expect.any(String), updated: expect.any(String), - number: 21, - picture: { - id: expect.any(Number), - created: expect.any(String), - updated: expect.any(String), - filename: expect.any(String), - mimetype: 'image/webp', - size: 117280, - }, + filename: expect.any(String), + picture_promotion_id: 21, + mimetype: 'image/webp', + size: 117280, }); // expect the file to be created on disk - expect(existsSync(join(env.PROMOTION_BASE_PATH, 'logo', (response.body as Promotion).picture.filename))).toBe( - true, - ); + const logo = await em.findOne(PromotionPicture, { picture_promotion: 21 }); + expect(existsSync(join(env.PROMOTION_BASE_PATH, 'logo', logo.filename))).toBe(true); }); it('when the promotion has a logo and update the logo', async () => { @@ -557,33 +548,28 @@ describe('Promotions (e2e)', () => { const response = await request(server) .post('/promotions/21/logo') .set('Authorization', `Bearer ${tokenPromotionModerator}`) - .attach('file', filePictureSquare, 'file.png'); + .attach('file', filePictureSquare, 'file.png') + .expect(201); // expect registered data to be returned expect(response.body).toEqual({ - id: 21, + id: expect.any(Number), created: expect.any(String), updated: expect.any(String), - number: 21, - picture: { - id: expect.any(Number), - created: expect.any(String), - updated: expect.any(String), - filename: expect.any(String), - description: null, - mimetype: 'image/webp', - size: 117280, - visibility: null, - }, + filename: expect.any(String), + picture_promotion_id: 21, + description: null, + mimetype: 'image/webp', + size: 117280, }); // expect the old file to be deleted from disk expect(existsSync(join(env.PROMOTION_BASE_PATH, 'logo', oldLogo.filename))).toBe(false); + const newLogo = await em.findOne(PromotionPicture, { picture_promotion: 21 }); + // expect the new file to be created on disk - expect(existsSync(join(env.PROMOTION_BASE_PATH, 'logo', (response.body as Promotion).picture.filename))).toBe( - true, - ); + expect(existsSync(join(env.PROMOTION_BASE_PATH, 'logo', newLogo.filename))).toBe(true); }); }); }); @@ -670,14 +656,12 @@ describe('Promotions (e2e)', () => { // Get the logo filename const response = await request(server) .delete('/promotions/21/logo') - .set('Authorization', `Bearer ${tokenPromotionModerator}`); + .set('Authorization', `Bearer ${tokenPromotionModerator}`) + .expect(200); expect(response.body).toEqual({ - id: 21, - created: expect.any(String), - updated: expect.any(String), - number: 21, - picture: undefined, + message: t.Success.Entity.Deleted(PromotionPicture), + statusCode: 200, }); // expect the file to be deleted from disk diff --git a/tests/e2e/roles.e2e-spec.ts b/tests/e2e/roles.e2e-spec.ts index 54a04c47..ac8a22a5 100644 --- a/tests/e2e/roles.e2e-spec.ts +++ b/tests/e2e/roles.e2e-spec.ts @@ -77,7 +77,7 @@ describe('Roles (e2e)', () => { permissions: expect.any(Array), revoked: false, updated: expect.any(String), - users: expect.any(Number), + users_count: expect.any(Number), }); }); }); @@ -185,6 +185,7 @@ describe('Roles (e2e)', () => { name: 'TEST_ROLE', revoked: false, permissions: ['ROOT'], + users_count: 0, }); }); }); @@ -284,7 +285,7 @@ describe('Roles (e2e)', () => { name: 'TEST_TEST_ROLE', revoked: false, permissions: ['ROOT', 'CAN_READ_ROLE', 'CAN_EDIT_ROLE'], - users: 0, + users_count: 0, }); }); }); @@ -366,7 +367,7 @@ describe('Roles (e2e)', () => { 'CAN_READ_PERMISSIONS_OF_ROLE', 'CAN_EDIT_PERMISSIONS_OF_ROLE', ], - users: 1, + users_count: 1, }); }); }); diff --git a/tests/e2e/users/users-data.e2e-spec.ts b/tests/e2e/users/users-data.e2e-spec.ts index 6168c740..e4077416 100644 --- a/tests/e2e/users/users-data.e2e-spec.ts +++ b/tests/e2e/users/users-data.e2e-spec.ts @@ -2,7 +2,6 @@ import type { email } from '#types'; import request from 'supertest'; -import { USER_GENDER } from '@exported/api/constants/genders'; import { TokenDTO } from '@modules/auth/dto/get.dto'; import { User } from '@modules/users/entities/user.entity'; @@ -239,19 +238,6 @@ describe('Users Data (e2e)', () => { updated: expect.any(String), first_name: 'John', last_name: 'Doe', - email_verified: true, - files_visibility_groups: [], - full_name: 'John Doe', - email: fakeUserEmail, - birth_date: '2001-01-01T00:00:00.000Z', - age: expect.any(Number), - is_minor: false, - gender: USER_GENDER[0], - last_seen: expect.any(String), - logs: [], - permissions: [], - roles: [], - verified: expect.any(String), }, ]); }); @@ -439,22 +425,12 @@ describe('Users Data (e2e)', () => { first_name: 'John', last_name: 'Doe', full_name: 'John Doe', - email: 'john.doe@example.fr', - email_verified: true, - gender: USER_GENDER[0], birth_date: '1990-01-01T00:00:00.000Z', age: expect.any(Number), is_minor: false, - last_seen: expect.any(String), nickname: null, - parent_contact: null, - phone: null, - banner: null, - picture: null, promotion: null, - pronouns: null, - secondary_email: null, - verified: expect.any(String), + last_seen: expect.any(String), }, ]); @@ -625,9 +601,6 @@ describe('Users Data (e2e)', () => { updated: expect.any(String), first_name: 'unverified', last_name: 'user', - email_verified: false, - picture: null, - banner: null, email: 'unverified@email.com', birth_date: '2000-01-01T00:00:00.000Z', nickname: null, @@ -641,7 +614,6 @@ describe('Users Data (e2e)', () => { full_name: 'unverified user', age: expect.any(Number), is_minor: false, - verified: null, }); }); @@ -657,9 +629,6 @@ describe('Users Data (e2e)', () => { updated: expect.any(String), first_name: 'root', last_name: 'root', - email_verified: true, - picture: null, - banner: null, email: 'ae.info@utbm.fr', birth_date: '2000-01-01T00:00:00.000Z', nickname: 'noot noot', @@ -673,7 +642,6 @@ describe('Users Data (e2e)', () => { full_name: 'root root', age: expect.any(Number), is_minor: false, - verified: expect.any(String), }); }); }); @@ -749,9 +717,6 @@ describe('Users Data (e2e)', () => { updated: expect.any(String), first_name: 'root', last_name: 'root', - email_verified: true, - picture: null, - banner: null, birth_date: expect.any(String), nickname: 'noot noot', promotion: 21, @@ -759,7 +724,6 @@ describe('Users Data (e2e)', () => { full_name: 'root root', age: expect.any(Number), is_minor: false, - verified: expect.any(String), }); }); @@ -775,9 +739,6 @@ describe('Users Data (e2e)', () => { updated: expect.any(String), first_name: 'root', last_name: 'root', - email_verified: true, - picture: null, - banner: null, birth_date: expect.any(String), nickname: 'noot noot', promotion: 21, @@ -785,7 +746,6 @@ describe('Users Data (e2e)', () => { full_name: 'root root', age: expect.any(Number), is_minor: false, - verified: expect.any(String), }); }); }); @@ -859,7 +819,7 @@ describe('Users Data (e2e)', () => { id: expect.any(Number), created: expect.any(String), updated: expect.any(String), - user: 1, + user_id: 1, email: false, secondary_email: false, birth_date: true, @@ -881,7 +841,7 @@ describe('Users Data (e2e)', () => { id: expect.any(Number), created: expect.any(String), updated: expect.any(String), - user: 3, + user_id: 3, email: false, secondary_email: false, birth_date: true, @@ -987,7 +947,7 @@ describe('Users Data (e2e)', () => { id: expect.any(Number), created: expect.any(String), updated: expect.any(String), - user: 1, + user_id: 1, email: true, secondary_email: true, birth_date: true, @@ -1019,7 +979,7 @@ describe('Users Data (e2e)', () => { id: expect.any(Number), created: expect.any(String), updated: expect.any(String), - user: 3, + user_id: 3, email: false, secondary_email: false, birth_date: true, @@ -1213,7 +1173,7 @@ describe('Users Data (e2e)', () => { name: 'ROOT', revoked: false, expires: '9999-12-31T00:00:00.000Z', - user: 1, + user_id: 1, }, ]); }); diff --git a/tests/e2e/users/users-files.e2e-spec.ts b/tests/e2e/users/users-files.e2e-spec.ts index e36435bc..1024747d 100644 --- a/tests/e2e/users/users-files.e2e-spec.ts +++ b/tests/e2e/users/users-files.e2e-spec.ts @@ -307,6 +307,8 @@ describe('Users Files (e2e)', () => { mimetype: 'image/webp', size: 3028, updated: expect.any(String), + picture_user_id: 4, + visibility_id: 1, }); // Delete the picture @@ -328,6 +330,8 @@ describe('Users Files (e2e)', () => { mimetype: 'image/webp', size: 3028, updated: expect.any(String), + picture_user_id: 4, + visibility_id: 1, }); // Delete the picture @@ -357,6 +361,8 @@ describe('Users Files (e2e)', () => { mimetype: 'image/webp', size: 3028, updated: expect.any(String), + picture_user_id: 1, + visibility_id: 1, }); // Delete the picture @@ -464,27 +470,8 @@ describe('Users Files (e2e)', () => { .expect(200); expect(response.body).toEqual({ - age: expect.any(Number), - banner: null, - birth_date: '2000-01-01T00:00:00.000Z', - created: expect.any(String), - email: 'ae.info@utbm.fr', - email_verified: true, - first_name: 'root', - full_name: 'root root', - gender: 'OTHER', - id: 1, - is_minor: false, - last_name: 'root', - last_seen: expect.any(String), - nickname: 'noot noot', - parent_contact: null, - phone: null, - promotion: 21, - pronouns: null, - secondary_email: null, - updated: expect.any(String), - verified: '2000-01-01T00:00:00.000Z', + message: t.Success.Entity.Deleted(UserPicture), + statusCode: 201, }); }); }); @@ -708,6 +695,8 @@ describe('Users Files (e2e)', () => { mimetype: 'image/webp', size: 9498, updated: expect.any(String), + banner_user_id: 4, + visibility_id: 1, }); // Delete the banner @@ -737,6 +726,8 @@ describe('Users Files (e2e)', () => { mimetype: 'image/webp', size: 9498, updated: expect.any(String), + banner_user_id: 4, + visibility_id: 1, }); // Delete the banner @@ -758,6 +749,8 @@ describe('Users Files (e2e)', () => { mimetype: 'image/webp', size: 9498, updated: expect.any(String), + banner_user_id: 1, + visibility_id: 1, }); // Delete the banner @@ -852,27 +845,8 @@ describe('Users Files (e2e)', () => { .expect(200); expect(response.body).toEqual({ - age: expect.any(Number), - birth_date: '2000-01-01T00:00:00.000Z', - created: expect.any(String), - email: 'ae.info@utbm.fr', - email_verified: true, - first_name: 'root', - full_name: 'root root', - gender: 'OTHER', - id: 1, - is_minor: false, - last_name: 'root', - last_seen: expect.any(String), - nickname: 'noot noot', - parent_contact: null, - phone: null, - picture: null, - promotion: 21, - pronouns: null, - secondary_email: null, - updated: expect.any(String), - verified: '2000-01-01T00:00:00.000Z', + message: t.Success.Entity.Deleted(UserBanner), + statusCode: 201, }); }); }); From a95b8de8b4f62fe0bdd11e5a3f979fd9549876a2 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Thu, 2 Nov 2023 02:57:04 +0100 Subject: [PATCH 04/15] fix(lint): eslint wtf ?? --- README.md | 28 +++++++++++++++------------- src/modules/auth/auth.service.ts | 6 +++--- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9a8884c2..8e246f33 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,12 @@ This repository contains the source code of the API used by the [Sith 4](https:/ ## Table of contents - [Installation](#installation) - - [Project](#project) - - [Environment variables](#environment-variables) - - [Database](#database) - - [PostgreSQL Installation](#postgresql-installation) - - [Configuration](#configuration) - - [First time setup](#first-time-setup) + - [Project](#project) + - [Environment variables](#environment-variables) + - [Database](#database) + - [PostgreSQL Installation](#postgresql-installation) + - [Configuration](#configuration) + - [First time setup](#first-time-setup) - [Launch](#launch) - [Tests](#tests) - [Linting](#linting) @@ -79,13 +79,13 @@ brew services start postgresql@13 # start postgresql service After the installation, you can use [pgAdmin](https://www.pgadmin.org/) to create a server with the following parameters: -| `.env` | pgAdmin 4 | value | -| :------------------: | :------------------: | :---------------------------------------------------------------------------------------- | -| `DB_HOST` | Host | `127.0.0.1` | -| `DB_PORT` | Port | `5432` | -| `DB_USERNAME` | Username | Should be the username you used to install postgresql or any user you have created for it | -| `DB_PASSWORD` | Password | leave it empty, unless you have set a password for your postgresql user | -| `DB_DATABASE` | Maintenance database | `postgres` | +| `.env` | pgAdmin 4 | value | +| :-----------: | :------------------: | :---------------------------------------------------------------------------------------- | +| `DB_HOST` | Host | `127.0.0.1` | +| `DB_PORT` | Port | `5432` | +| `DB_USERNAME` | Username | Should be the username you used to install postgresql or any user you have created for it | +| `DB_PASSWORD` | Password | leave it empty, unless you have set a password for your postgresql user | +| `DB_DATABASE` | Maintenance database | `postgres` | > **Note** > You can also use [TablePlus](https://tableplus.com/) to manage your databases as a lightweight (but more limited, in the free edition) alternative to pgAdmin. @@ -121,6 +121,7 @@ pnpm run start:prod ``` ## Tests + Both unit and e2e tests are available and run with [Jest](https://jestjs.io/). You can run them with the following command: ```bash @@ -131,6 +132,7 @@ pnpm test > After running the tests, a coverage report is generated in the `./coverage` folder. ## Linting + This project uses [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) to lint the code. You can run the linter with the following command: ```bash diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index dc77631f..60b371bb 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,4 +1,4 @@ -import type { email } from '#types'; +import type { email as Email } from '#types'; import type { JWTPayload } from '#types/api'; import { CreateRequestContext, MikroORM } from '@mikro-orm/core'; @@ -22,12 +22,12 @@ export class AuthService { /** * Sign in a user and return a JWT token and the user ID - * @param {email} email the user email + * @param {Email} email the user email * @param {string} pass the user password (hashed or not) @default false * @returns {Promise} The JWT token and the user ID */ @CreateRequestContext() - async signIn(email: email, pass: string): Promise { + async signIn(email: Email, pass: string): Promise { const user: User = await this.orm.em.findOne(User, { email }, { fields: ['*', 'password'] }); if (!user) throw new NotFoundException(this.t.Errors.Email.NotFound(User, email)); From 2a15c9aa6456c767f4f66b6bb0c5d156d8dc7a4a Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Tue, 7 Nov 2023 18:32:10 +0100 Subject: [PATCH 05/15] refactor(validation): add DTO validation & remove translate service --- src/app.module.ts | 2 - src/database/seeders/tests.seeder.ts | 2 +- src/env.ts | 7 + src/exported | 2 +- src/i18n/en-US/responses.json | 75 --- src/i18n/en-US/validations.json | 159 +++++++ src/i18n/fr-FR/responses.json | 72 --- src/main.ts | 6 +- ...r.decorator.ts => api-not-ok.decorator.ts} | 0 src/modules/_mixin/decorators/index.ts | 9 + .../_mixin/decorators/is-boolean.decorator.ts | 13 + .../_mixin/decorators/is-date.decorator.ts | 16 + .../_mixin/decorators/is-email.decorator.ts | 42 ++ .../_mixin/decorators/is-id.decorator.ts | 41 ++ .../_mixin/decorators/is-phone.decorator.ts | 13 + .../_mixin/decorators/is-string.decorator.ts | 13 + .../decorators/is-strong-pass.decorator.ts | 93 ++++ src/modules/_mixin/dto/base.dto.ts | 21 - src/modules/_mixin/dto/error.dto.ts | 18 - src/modules/_mixin/dto/input.dto.ts | 9 + src/modules/_mixin/dto/message.dto.ts | 20 - src/modules/_mixin/dto/output.dto.ts | 63 +++ src/modules/_mixin/entities/base.entity.ts | 8 +- src/modules/_mixin/http-errors/bad-request.ts | 8 + src/modules/_mixin/http-errors/base.ts | 89 ++++ src/modules/_mixin/http-errors/forbidden.ts | 8 + src/modules/_mixin/http-errors/index.ts | 5 + src/modules/_mixin/http-errors/not-found.ts | 8 + .../_mixin/http-errors/unauthorized.ts | 8 + src/modules/auth/auth.controller.ts | 56 +-- src/modules/auth/auth.module.ts | 3 +- src/modules/auth/auth.service.ts | 43 +- .../decorators/self-or-sub-perms.decorator.ts | 15 - .../self-or-subscribed.decorator.ts | 9 - src/modules/auth/dto/input.dto.ts | 73 +++ .../auth/dto/{get.dto.ts => output.dto.ts} | 7 +- src/modules/auth/dto/post.dto.ts | 39 -- .../auth/guards/self-or-perms.guard.ts | 4 +- .../auth/guards/self-or-sub-or-perms.guard.ts | 29 -- .../auth/guards/self-or-subscribed.guard.ts | 26 -- src/modules/auth/guards/self.guard.ts | 53 ++- src/modules/auth/guards/subscribed.guard.ts | 42 -- src/modules/auth/strategies/jwt.strategy.ts | 19 +- src/modules/emails/emails.module.ts | 4 +- src/modules/emails/emails.service.ts | 45 +- .../files/dto/{get.dto.ts => output.dto.ts} | 20 +- src/modules/files/files.module.ts | 3 +- src/modules/files/files.service.ts | 97 ++-- src/modules/files/images.service.ts | 34 +- .../logs/dto/{get.dto.ts => output.dto.ts} | 19 +- src/modules/logs/logs.controller.ts | 35 +- src/modules/logs/logs.module.ts | 3 +- src/modules/logs/logs.service.ts | 14 +- src/modules/permissions/dto/get.dto.ts | 35 -- src/modules/permissions/dto/input.dto.ts | 57 +++ src/modules/permissions/dto/output.dto.ts | 28 ++ src/modules/permissions/dto/patch.dto.ts | 28 -- src/modules/permissions/dto/post.dto.ts | 20 - .../permissions/permissions.controller.ts | 59 +-- src/modules/permissions/permissions.module.ts | 3 +- .../permissions/permissions.service.ts | 42 +- src/modules/promotions/dto/get.dto.ts | 27 -- src/modules/promotions/dto/input.dto.ts | 9 + src/modules/promotions/dto/output.dto.ts | 22 + .../promotions/promotions.controller.ts | 86 ++-- src/modules/promotions/promotions.module.ts | 12 +- src/modules/promotions/promotions.service.ts | 66 ++- src/modules/roles/dto/get.dto.ts | 31 -- src/modules/roles/dto/input.dto.ts | 72 +++ src/modules/roles/dto/output.dto.ts | 25 + src/modules/roles/dto/patch.dto.ts | 22 - src/modules/roles/dto/post.dto.ts | 16 - src/modules/roles/roles.controller.ts | 108 ++--- src/modules/roles/roles.module.ts | 3 +- src/modules/roles/roles.service.ts | 63 ++- src/modules/translate/translate.module.ts | 10 - src/modules/translate/translate.service.ts | 118 ----- .../controllers/users-data.controller.ts | 163 +++---- .../controllers/users-files.controller.ts | 112 ++--- src/modules/users/dto/base-user.dto.ts | 16 - src/modules/users/dto/get.dto.ts | 263 ----------- src/modules/users/dto/input.dto.ts | 106 +++++ src/modules/users/dto/output.dto.ts | 142 ++++++ src/modules/users/dto/patch.dto.ts | 6 - .../users/entities/user-visibility.entity.ts | 2 +- src/modules/users/entities/user.entity.ts | 9 +- .../users/services/users-data.service.ts | 161 +++---- .../users/services/users-files.service.ts | 75 ++- src/modules/users/users.module.ts | 12 +- src/utils/dates.ts | 5 +- src/utils/password.ts | 45 -- src/utils/validate.ts | 23 - tests/e2e/auth.e2e-spec.ts | 162 +++---- tests/e2e/logs.e2e-spec.ts | 21 +- tests/e2e/permissions.e2e-spec.ts | 55 +-- tests/e2e/promotions.e2e-spec.ts | 71 +-- tests/e2e/roles.e2e-spec.ts | 210 ++++----- tests/e2e/users/users-data.e2e-spec.ts | 435 ++++++++---------- tests/e2e/users/users-files.e2e-spec.ts | 188 ++++---- tests/index.ts | 14 +- tests/units/services/auth.test.ts | 9 +- tests/units/services/files.test.ts | 26 +- tests/units/services/images.test.ts | 24 + tests/units/services/users/users-data.test.ts | 25 + tests/units/utils/password.test.ts | 15 +- 105 files changed, 2252 insertions(+), 2627 deletions(-) delete mode 100644 src/i18n/en-US/responses.json create mode 100644 src/i18n/en-US/validations.json delete mode 100644 src/i18n/fr-FR/responses.json rename src/modules/_mixin/decorators/{error.decorator.ts => api-not-ok.decorator.ts} (100%) create mode 100644 src/modules/_mixin/decorators/index.ts create mode 100644 src/modules/_mixin/decorators/is-boolean.decorator.ts create mode 100644 src/modules/_mixin/decorators/is-date.decorator.ts create mode 100644 src/modules/_mixin/decorators/is-email.decorator.ts create mode 100644 src/modules/_mixin/decorators/is-id.decorator.ts create mode 100644 src/modules/_mixin/decorators/is-phone.decorator.ts create mode 100644 src/modules/_mixin/decorators/is-string.decorator.ts create mode 100644 src/modules/_mixin/decorators/is-strong-pass.decorator.ts delete mode 100644 src/modules/_mixin/dto/base.dto.ts delete mode 100644 src/modules/_mixin/dto/error.dto.ts create mode 100644 src/modules/_mixin/dto/input.dto.ts delete mode 100644 src/modules/_mixin/dto/message.dto.ts create mode 100644 src/modules/_mixin/dto/output.dto.ts create mode 100644 src/modules/_mixin/http-errors/bad-request.ts create mode 100644 src/modules/_mixin/http-errors/base.ts create mode 100644 src/modules/_mixin/http-errors/forbidden.ts create mode 100644 src/modules/_mixin/http-errors/index.ts create mode 100644 src/modules/_mixin/http-errors/not-found.ts create mode 100644 src/modules/_mixin/http-errors/unauthorized.ts delete mode 100644 src/modules/auth/decorators/self-or-sub-perms.decorator.ts delete mode 100644 src/modules/auth/decorators/self-or-subscribed.decorator.ts create mode 100644 src/modules/auth/dto/input.dto.ts rename src/modules/auth/dto/{get.dto.ts => output.dto.ts} (50%) delete mode 100644 src/modules/auth/dto/post.dto.ts delete mode 100644 src/modules/auth/guards/self-or-sub-or-perms.guard.ts delete mode 100644 src/modules/auth/guards/self-or-subscribed.guard.ts delete mode 100644 src/modules/auth/guards/subscribed.guard.ts rename src/modules/files/dto/{get.dto.ts => output.dto.ts} (55%) rename src/modules/logs/dto/{get.dto.ts => output.dto.ts} (67%) delete mode 100644 src/modules/permissions/dto/get.dto.ts create mode 100644 src/modules/permissions/dto/input.dto.ts create mode 100644 src/modules/permissions/dto/output.dto.ts delete mode 100644 src/modules/permissions/dto/patch.dto.ts delete mode 100644 src/modules/permissions/dto/post.dto.ts delete mode 100644 src/modules/promotions/dto/get.dto.ts create mode 100644 src/modules/promotions/dto/input.dto.ts create mode 100644 src/modules/promotions/dto/output.dto.ts delete mode 100644 src/modules/roles/dto/get.dto.ts create mode 100644 src/modules/roles/dto/input.dto.ts create mode 100644 src/modules/roles/dto/output.dto.ts delete mode 100644 src/modules/roles/dto/patch.dto.ts delete mode 100644 src/modules/roles/dto/post.dto.ts delete mode 100644 src/modules/translate/translate.module.ts delete mode 100644 src/modules/translate/translate.service.ts delete mode 100644 src/modules/users/dto/base-user.dto.ts delete mode 100644 src/modules/users/dto/get.dto.ts create mode 100644 src/modules/users/dto/input.dto.ts create mode 100644 src/modules/users/dto/output.dto.ts delete mode 100644 src/modules/users/dto/patch.dto.ts delete mode 100644 src/utils/password.ts delete mode 100644 src/utils/validate.ts create mode 100644 tests/units/services/images.test.ts diff --git a/src/app.module.ts b/src/app.module.ts index b83d5967..ba30e587 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,7 +14,6 @@ import { LogsModule } from '@modules/logs/logs.module'; import { PermissionsModule } from '@modules/permissions/permissions.module'; import { PromotionsModule } from '@modules/promotions/promotions.module'; import { RolesModule } from '@modules/roles/roles.module'; -import { TranslateModule } from '@modules/translate/translate.module'; import { UsersModule } from '@modules/users/users.module'; @Module({ @@ -37,7 +36,6 @@ import { UsersModule } from '@modules/users/users.module'; PromotionsModule, RolesModule, ScheduleModule.forRoot(), - TranslateModule, UsersModule, ], providers: [ diff --git a/src/database/seeders/tests.seeder.ts b/src/database/seeders/tests.seeder.ts index e5f8f2b9..e8a90ef4 100644 --- a/src/database/seeders/tests.seeder.ts +++ b/src/database/seeders/tests.seeder.ts @@ -111,7 +111,7 @@ export class TestSeeder extends Seeder { { email: 'unverified@email.com', email_verified: false, - email_verification: hashSync('token67891012', 10), + email_verification: hashSync('token67891012', 10), // used in auth.e2e-spec.ts password: hashSync('root', 10), first_name: 'unverified', last_name: 'user', diff --git a/src/env.ts b/src/env.ts index 385f2247..14f80ed6 100644 --- a/src/env.ts +++ b/src/env.ts @@ -4,8 +4,15 @@ import { join } from 'node:path'; import 'dotenv/config'; import { Logger } from '@nestjs/common'; +import { I18nValidationPipeOptions } from 'nestjs-i18n'; import { z } from 'zod'; +export const VALIDATION_PIPE_OPTIONS: I18nValidationPipeOptions = { + transform: true, + whitelist: true, + forbidNonWhitelisted: true, +}; + /** * NodeJS environment variables + API specific environment variables, validated using zod * @see https://github.com/colinhacks/zod diff --git a/src/exported b/src/exported index 90076abb..6c36d852 160000 --- a/src/exported +++ b/src/exported @@ -1 +1 @@ -Subproject commit 90076abb92e4971e680cbd0194ccfa5876a17658 +Subproject commit 6c36d852855799d808611bb094d303bc21dc5206 diff --git a/src/i18n/en-US/responses.json b/src/i18n/en-US/responses.json deleted file mode 100644 index 793e0157..00000000 --- a/src/i18n/en-US/responses.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "errors": { - "birth_date": { - "invalid": "The date '{date}' is invalid, either too young or in the future." - }, - "email": { - "already_verified": "{type} email is already verified.", - "blacklisted": "Email '{email}' is blacklisted and cannot be used.", - "malformed": "Email '{email}' is either too long (> 60) or too short (< 6)", - "invalid": "The given email '{email}' does not respect the email format", - "used": "Email '{email}' is already taken.", - "are_used": "Emails '{emails}' are already taken.", - "not_found": "{type} with email '{email}' not found.", - "unverified": "{type} email is not verified.", - "token": { - "invalid": "Invalid email verification token." - } - }, - "entity": { - "not_found": "Entity '{type}' not found using '{field}' with value '{value}'." - }, - "file": { - "infected": "The given file '{file}' is infected by a virus.", - "invalid_mime_type": "The given file has not one of the expected MIME type : '[{mime_type}]'.", - "no_file": "No file was provided.", - "not_found_on_disk": "File '{file}' not found on disk.", - "undefined_mime_type": "The given file has an undefined MIME type.", - "unauthorized": "You need to be in the '{visibility_group}' visibility group to access this file." - }, - "id": { - "invalid": "{type} ID '{id}' is invalid.", - "invalids": "{type}(s) with ID(s) in '[{ids}]' are invalid.", - "not_found": "{type} with ID '{id}' not found.", - "not_founds": "{type}(s) with ID(s) in '[{ids}]' not found." - }, - "image": { - "invalid_aspect_ratio": "The given image does not respect the expected '{aspect_ratio}' aspect ratio." - }, - "jwt": { - "invalid": "The JWT token is invalid.", - "expired": "The JWT token has expired.", - "unknown": "An unknown error occurred while verifying the JWT token." - }, - "password": { - "mismatch": "Password mismatch", - "weak": "Password is too weak. Please use a stronger password with at least 8 characters and a mix of letters, numbers and special characters." - }, - "permission": { - "invalid": "Permission '{permission}' does not exist.", - "not_found_on_user": "Permission '{permission}' not found on user '{user}'.", - "already_on_user": "User '{user}' has already the '{permission}' permission." - }, - "promotion": { - "logo_not_found": "The promotion '{number}' does not have a logo." - }, - "role": { - "name_used": "Role name '{name}' is already taken." - }, - "user": { - "cannot_update_birth_date_or_name": "You cannot update your own birth date/name, ask another user with the appropriate permission", - "picture_cooldown": "You cannot update your picture before {time_left} ms.", - "no_picture": "User '{user_id}' does not have a picture.", - "no_banner": "User '{user_id}' does not have a banner." - } - }, - "success": { - "deleted": "'{type}' successfully deleted.", - "email": { - "verified": "{type} email has been verified." - }, - "user": { - "register": "You have been successfully registered, please check your email to verify your account." - } - } -} diff --git a/src/i18n/en-US/validations.json b/src/i18n/en-US/validations.json new file mode 100644 index 00000000..b155c97a --- /dev/null +++ b/src/i18n/en-US/validations.json @@ -0,0 +1,159 @@ +{ + "array": { + "invalid": { + "format": "{property} with value '{value}' is not a valid array of '{type}'", + "duplicate": "{property} with value '{value}' is not a valid array, it contains duplicate values", + "not_empty": "{property} is not a valid array, it must not be empty" + } + }, + "boolean": { + "invalid": { + "format": "{property} with value '{value}' is not a valid boolean" + } + }, + "birth_date": { + "invalid": { + "outbound": "'{date}' is not a valid birth date, it must be at least 13 years ago and at most 100" + } + }, + "date": { + "invalid": { + "format": "{property} with value '{value}' is not a valid date" + } + }, + "email": { + "invalid": { + "format": "{property} with value '{value}' is not a valid email", + "blacklisted": "{property} with value '{value}' is a blacklisted email", + "size": "Email '{email}' is either too long (>60) or too short (<6)", + "used": "Email '{email}' is already used", + "are_used": "Emails ['{emails}'] are already used", + "already_verified": "Email already verified" + }, + "success": { + "verified": "Successfully verified email" + } + }, + "file": { + "invalid": { + "not_provided": "File not provided", + "not_found": "File '{filename}' cannot be found on disk", + "no_mime_type": "File has no mime type", + "unauthorized_mime_type": "File has an unauthorized mime type, valid mime types are ['{mime_types}']", + "infected": "File is infected with virus" + } + }, + "file_visibility_group": { + "invalid": { + "not_found": "File visibility group with name '{name}' not found" + } + }, + "gender": { + "invalid": { + "format": "{property} with value '{value}' is not a valid gender, valid genders are ['{genders}']" + } + }, + "id": { + "invalid": { + "format": "{property} with value '{value}' is not a valid ID, it must be a valid string number or a number", + "min": "{property} with value '{value}' is not a valid ID, it must be greater than or equal to {min}" + } + }, + "ids": { + "invalid": { + "format": "{property} with values in ['{value}'] is not a valid IDs array, it must be an array of valid string numbers and/or numbers" + } + }, + "image": { + "invalid": { + "aspect_ratio": "Image aspect ratio must be {aspect_ratio}" + } + }, + "logs": { + "success": { + "deleted": "Successfully deleted logs" + } + }, + "number": { + "invalid": { + "format": "{property} with value '{value}' is not a valid string number or number", + "min": "{property} with value '{value}' is not a valid number, it must be greater than or equal to {min}" + } + }, + "password": { + "invalid": { + "mismatch": "Passwords do not match", + "weak": "{property} with value '{value}' is not a valid password, it must be at least 8 characters long and contain at least one number, one lowercase letter, one uppercase letter and one special character" + } + }, + "permission": { + "invalid": { + "format": "{property} with value '{value}' is not a valid permission, valid permissions are: ['{permissions}']", + "already_on": "Permission '{permission}' already attributed to user '{name}'" + }, + "not_found": "Permission with ID '{id}' not found on user '{name}'" + }, + "phone": { + "invalid": { + "format": "{property} with value '{value}' is not a valid phone number, it must start with a '+' followed by a country code and contain only numbers" + } + }, + "promotion": { + "invalid": { + "not_found": "Promotion with number '{number}' not found", + "no_logo": "Promotion with number '{number}' has no logo" + }, + "success": { + "deleted_logo": "Successfully deleted logo of promotion '{number}'" + } + }, + "role": { + "invalid": { + "already_exist": "Role with name '{name}' already exist", + "already_on": "Role '{role}' already attributed to user '{name}'" + }, + "not_found": "Role with ID '{id}' not found" + }, + "string": { + "invalid": { + "format": "{property} with value '{value}' is not a valid string", + "uppercase": "{property} with value '{value}' is not a valid string, it must be uppercase" + } + }, + "token": { + "invalid": { + "format": "Token invalid", + "expired": "Token expired", + "unknown": "An unknown error occurred while verifying token" + } + }, + "user": { + "invalid": { + "not_in_file_visibility_group": "User is not in file visibility group '{group_name}'" + }, + "success": { + "registered": "User '{name}' successfully registered", + "deleted": "User '{name}' successfully deleted", + "deleted_picture": "Successfully deleted picture of '{name}'", + "deleted_banner": "Successfully deleted banner of '{name}'" + }, + "cannot_update": "You cannot update your own birth date or name, ask another user with the appropriate permission", + "not_found": { + "email": "User with email '{email}' not found", + "id": "User with ID '{id}' not found" + }, + "unverified": "User unverified", + "picture": { + "cooldown": "You can only change your picture once every {days} days", + "not_found": "User '{name}' has no picture" + }, + "banner": { + "not_found": "User '{name}' has no banner" + } + }, + "users": { + "not_found": { + "ids": "User(s) within IDs ['{ids}'] not found" + } + } +} diff --git a/src/i18n/fr-FR/responses.json b/src/i18n/fr-FR/responses.json deleted file mode 100644 index 0f36f63f..00000000 --- a/src/i18n/fr-FR/responses.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "errors": { - "birth_date": { - "invalid": "La date '{date}' n'est pas valide, soit trop jeune, soit dans le futur." - }, - "email": { - "already_verified": "L'email {type} est déjà vérifié.", - "blacklisted": "L'email '{email}' est sur la liste noire et ne peut pas être utilisé.", - "malformed": "L'email '{email}' est soit trop long (> 60) soit trop court (< 6)", - "invalid": "L'email '{email}' ne respecte pas le format d'email", - "used": "L'email '{email}' est déjà pris.", - "are_used": "Les emails '{emails}' sont déjà pris.", - "not_found": "{type} avec l'email '{email}' introuvable.", - "unverified": "L'email de {type} n'est pas vérifié.", - "token": { - "invalid": "Jeton de vérification d'email invalide." - } - }, - "entity": { - "not_found": "Entité '{type}' non trouvée en utilisant '{field}' avec la valeur '{value}'." - }, - "file": { - "infected": "Le fichier '{file}' est infecté par un virus.", - "invalid_mime_type": "Le fichier donné n'a pas un des types MIME attendus : '[{mime_type}]'.", - "no_file": "Aucun fichier n'a été fourni.", - "not_found_on_disk": "Fichier '{file}' introuvable sur le disque.", - "undefined_mime_type": "Le fichier donné a un type MIME non défini.", - "unauthorized": "Vous devez être dans le groupe de visibilité '{visibility_group}' pour accéder à ce fichier." - }, - "id": { - "invalid": "L'ID '{id}' de {type} n'est pas valide.", - "invalids": "Le(s) {type}(s) avec les ID(s) dans '[{ids}]' sont invalides.", - "not_found": "{type} avec l'ID '{id}' introuvable.", - "not_founds": "{type}(s) avec le(s) ID(s) dans '[{ids}]' introuvable." - }, - "image": { - "invalid_aspect_ratio": "L'image donnée ne respecte pas le rapport d'aspect '{aspect_ratio}' attendu." - }, - "jwt": { - "invalid": "Le jeton JWT est invalide.", - "expired": "Le jeton JWT a expiré.", - "unknown": "Une erreur inconnue s'est produite lors de la vérification du jeton JWT." - }, - "password": { - "mismatch": "Mots de passe différents", - "weak": "Le mot de passe est trop faible. Veuillez utiliser un mot de passe plus fort avec au moins 8 caractères et un mélange de lettres, chiffres et caractères spéciaux." - }, - "permission": { - "invalid": "La permission '{permission}' n'existe pas.", - "not_found_on_user": "Permission '{permission}' introuvable sur l'utilisateur '{user}'.", - "already_on_user": "L'utilisateur '{user}' a déjà la permission '{permission}'." - }, - "promotion": { - "logo_not_found": "La promotion '{number}' n'a pas de logo." - }, - "role": { - "name_used": "Le nom de rôle '{name}' est déjà pris." - }, - "user": { - "cannot_update_birth_date_or_name": "Vous ne pouvez pas mettre à jour votre propre nom/date de naissance, demandez à un autre utilisateur avec la permission appropriée", - "picture_cooldown": "Vous ne pouvez pas mettre à jour votre photo avant {time_left} ms.", - "no_picture": "L'utilisateur '{user_id}' n'a pas de photo.", - "no_banner": "L'utilisateur '{user_id}' n'a pas de bannière." - } - }, - "success": { - "deleted": "'{type}' supprimé avec succès.", - "email": { - "verified": "L'email de {type} a été vérifié." - } - } -} diff --git a/src/main.ts b/src/main.ts index 3880ef94..56b46f37 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,8 +2,10 @@ import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n'; -import { env } from '@env'; +import { VALIDATION_PIPE_OPTIONS, env } from '@env'; +import { I18nHttpExceptionFilter } from '@modules/_mixin/http-errors'; import '@exported/global/utils'; import { AppModule } from './app.module'; @@ -18,6 +20,8 @@ async function bootstrap() { app.enableCors({ origin: cors_urls.includes('*') ? '*' : cors_urls }); app.useStaticAssets('./src/swagger', { index: false, prefix: '/public' }); + app.useGlobalPipes(new I18nValidationPipe(VALIDATION_PIPE_OPTIONS)); + app.useGlobalFilters(new I18nValidationExceptionFilter({ detailedErrors: false }), new I18nHttpExceptionFilter()); const config = new DocumentBuilder() .setTitle('AE UTBM — REST API') diff --git a/src/modules/_mixin/decorators/error.decorator.ts b/src/modules/_mixin/decorators/api-not-ok.decorator.ts similarity index 100% rename from src/modules/_mixin/decorators/error.decorator.ts rename to src/modules/_mixin/decorators/api-not-ok.decorator.ts diff --git a/src/modules/_mixin/decorators/index.ts b/src/modules/_mixin/decorators/index.ts new file mode 100644 index 00000000..a00785a4 --- /dev/null +++ b/src/modules/_mixin/decorators/index.ts @@ -0,0 +1,9 @@ +export * from './api-not-ok.decorator'; + +export * from './is-boolean.decorator'; +export * from './is-date.decorator'; +export * from './is-email.decorator'; +export * from './is-id.decorator'; +export * from './is-phone.decorator'; +export * from './is-string.decorator'; +export * from './is-strong-pass.decorator'; diff --git a/src/modules/_mixin/decorators/is-boolean.decorator.ts b/src/modules/_mixin/decorators/is-boolean.decorator.ts new file mode 100644 index 00000000..6d194fbe --- /dev/null +++ b/src/modules/_mixin/decorators/is-boolean.decorator.ts @@ -0,0 +1,13 @@ +import type { I18nTranslations } from '#types/api'; + +import { applyDecorators } from '@nestjs/common'; +import { IsBoolean } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +export const I18nIsBoolean = () => { + return applyDecorators( + IsBoolean({ + message: i18nValidationMessage('validations.boolean.invalid.format'), + }), + ); +}; diff --git a/src/modules/_mixin/decorators/is-date.decorator.ts b/src/modules/_mixin/decorators/is-date.decorator.ts new file mode 100644 index 00000000..b7ca85d4 --- /dev/null +++ b/src/modules/_mixin/decorators/is-date.decorator.ts @@ -0,0 +1,16 @@ +import type { I18nTranslations } from '#types/api'; + +import { applyDecorators } from '@nestjs/common'; +import { IsDateString } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +export const I18nIsDate = () => { + return applyDecorators( + IsDateString( + {}, + { + message: i18nValidationMessage('validations.date.invalid.format'), + }, + ), + ); +}; diff --git a/src/modules/_mixin/decorators/is-email.decorator.ts b/src/modules/_mixin/decorators/is-email.decorator.ts new file mode 100644 index 00000000..de44359c --- /dev/null +++ b/src/modules/_mixin/decorators/is-email.decorator.ts @@ -0,0 +1,42 @@ +import type { email } from '#types'; +import type { I18nTranslations } from '#types/api'; + +import { applyDecorators } from '@nestjs/common'; +import { IsEmail, ValidationOptions, registerDecorator } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +import { EmailsService } from '@modules/emails/emails.service'; + +/** + * Custom class validator to check if the email can be used or not. + * (following AE's rules) + * + * @remark we use our own custom blacklist because we want to be able to blacklist hosts, emails but + * also whitelist some of them. + */ +function IsEmailAuthorized(validationOptions?: ValidationOptions): PropertyDecorator { + return function (object: object, propertyName: string | symbol) { + registerDecorator({ + name: 'isEmail', + target: object.constructor, + propertyName: propertyName as string, + constraints: [], + options: validationOptions, + validator: { + validate(value: unknown) { + return typeof value === 'string' && !EmailsService.isEmailBlacklisted(value as email); + }, + }, + }); + }; +} + +/** + * Check if the email is valid and not blacklisted, using the blacklisted defined in the env. + */ +export const I18nIsEmail = () => { + return applyDecorators( + IsEmailAuthorized({ message: i18nValidationMessage('validations.email.invalid.blacklisted') }), + IsEmail({}, { message: i18nValidationMessage('validations.email.invalid.format') }), + ); +}; diff --git a/src/modules/_mixin/decorators/is-id.decorator.ts b/src/modules/_mixin/decorators/is-id.decorator.ts new file mode 100644 index 00000000..7724aa2e --- /dev/null +++ b/src/modules/_mixin/decorators/is-id.decorator.ts @@ -0,0 +1,41 @@ +import type { I18nTranslations } from '#types/api'; + +import { applyDecorators } from '@nestjs/common'; +import { ValidationOptions, isNumber, isNumberString, registerDecorator } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +export const I18nIsId = (validationOptions?: ValidationOptions) => { + return applyDecorators( + IsNumberStringOrNumber({ + message: i18nValidationMessage('validations.id.invalid.format'), + ...validationOptions, + }), + ); +}; + +/** + * Validate if the value is a number or a number string. + * + * @example value = 1 => true + * @example value = '1' => true + * @example value = 'invalid' => false + */ +export const IsNumberStringOrNumber = (validationOptions?: ValidationOptions): PropertyDecorator => { + return function (object: object, propertyName: string | symbol) { + registerDecorator({ + name: 'isNumberStringOrNumber', + target: object.constructor, + propertyName: propertyName as string, + constraints: [], + options: validationOptions, + validator: { + validate(value: unknown) { + if (typeof value !== 'string' && typeof value !== 'number') return false; + + if (typeof value === 'string') return isNumberString(value, { no_symbols: true }); + return isNumber(value); + }, + }, + }); + }; +}; diff --git a/src/modules/_mixin/decorators/is-phone.decorator.ts b/src/modules/_mixin/decorators/is-phone.decorator.ts new file mode 100644 index 00000000..4271cabc --- /dev/null +++ b/src/modules/_mixin/decorators/is-phone.decorator.ts @@ -0,0 +1,13 @@ +import type { I18nTranslations } from '#types/api'; + +import { applyDecorators } from '@nestjs/common'; +import { IsPhoneNumber } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +export const I18nIsPhoneNumber = () => { + return applyDecorators( + IsPhoneNumber(undefined, { + message: i18nValidationMessage('validations.phone.invalid.format'), + }), + ); +}; diff --git a/src/modules/_mixin/decorators/is-string.decorator.ts b/src/modules/_mixin/decorators/is-string.decorator.ts new file mode 100644 index 00000000..bdcb3197 --- /dev/null +++ b/src/modules/_mixin/decorators/is-string.decorator.ts @@ -0,0 +1,13 @@ +import type { I18nTranslations } from '#types/api'; + +import { applyDecorators } from '@nestjs/common'; +import { IsString } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +export const I18nIsString = () => { + return applyDecorators( + IsString({ + message: i18nValidationMessage('validations.string.invalid.format'), + }), + ); +}; diff --git a/src/modules/_mixin/decorators/is-strong-pass.decorator.ts b/src/modules/_mixin/decorators/is-strong-pass.decorator.ts new file mode 100644 index 00000000..38d46013 --- /dev/null +++ b/src/modules/_mixin/decorators/is-strong-pass.decorator.ts @@ -0,0 +1,93 @@ +import type { I18nTranslations } from '#types/api'; + +import { applyDecorators } from '@nestjs/common'; +import { ValidationOptions, registerDecorator } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +import { env } from '@env'; +import { randomInt } from '@exported/global/utils'; + +const SYMBOL_CHARS = '!@#$%^&*()'; +const LOWERCASE_CHARS = 'abcdefghijklmnopqrstuvwxyz'; +const UPPERCASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +const NUMBERS = '0123456789'; + +export const MINIMUM_PASSWORD_LENGTH = 8; +export const MINIMUM_SYMBOLS = 1; +export const MINIMUM_LOWERCASE = 1; +export const MINIMUM_UPPERCASE = 1; +export const MINIMUM_NUMBERS = 1; + +/** + * Generates a random password of the given length. + * @param {number} length the length of the password to generate + * @returns {string} the generated password + * + * TODO move that file to somewhere else? + */ +export function generateRandomPassword(length: number = MINIMUM_PASSWORD_LENGTH): string { + if (length < MINIMUM_PASSWORD_LENGTH) length = MINIMUM_PASSWORD_LENGTH; + + const password = [ + SYMBOL_CHARS[randomInt(SYMBOL_CHARS.length - 1)], + LOWERCASE_CHARS[randomInt(LOWERCASE_CHARS.length - 1)], + UPPERCASE_CHARS[randomInt(UPPERCASE_CHARS.length - 1)], + NUMBERS[randomInt(NUMBERS.length - 1)], + ].shuffle(); + + const remainingLength = length - password.length; + + for (let i = 0; i < remainingLength; i++) { + const charSet = SYMBOL_CHARS + LOWERCASE_CHARS + UPPERCASE_CHARS + NUMBERS; + password.push(charSet[randomInt(charSet.length - 1)]); + } + + return password.join(''); +} + +/** + * Check if the password is strong enough + * @param {string} password the password to check + * @returns {boolean} true if the password is strong enough, false otherwise + */ +export function isStrongPassword(password: string): boolean { + if (env.DEBUG && password === 'root') return true; // Allow 'root' password in DEBUG mode (tests & dev) + + const regex = new RegExp( + `^(?=.*[${LOWERCASE_CHARS}])(?=.*[${UPPERCASE_CHARS}])(?=.*[${NUMBERS}])(?=.*[${SYMBOL_CHARS}]).{${MINIMUM_PASSWORD_LENGTH},}$`, + ); + return regex.test(password); +} + +/** + * Custom class validator to check if the password is strong enough + * @remark If env.DEBUG is true, allow 'root' password (for tests & dev) + */ +export function IsStrongPassword(validationOptions?: ValidationOptions): PropertyDecorator { + return function (object: object, propertyName: string | symbol) { + registerDecorator({ + name: 'isStrongPassword', + target: object.constructor, + propertyName: propertyName as string, + constraints: [], + options: validationOptions, + validator: { + validate(value: unknown) { + return typeof value === 'string' && isStrongPassword(value); + }, + }, + }); + }; +} + +/** + * Validate the password strength + * @remark If env.DEBUG is true, allow 'root' password (for tests & dev) + */ +export const I18nIsStrongPassword = () => { + return applyDecorators( + IsStrongPassword({ + message: i18nValidationMessage('validations.password.invalid.weak'), + }), + ); +}; diff --git a/src/modules/_mixin/dto/base.dto.ts b/src/modules/_mixin/dto/base.dto.ts deleted file mode 100644 index 3aad392f..00000000 --- a/src/modules/_mixin/dto/base.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IBaseResponseDTO } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsDate, IsInt } from 'class-validator'; - -/** - * Base response DTO class - */ -export abstract class BaseResponseDTO implements IBaseResponseDTO { - @ApiProperty({ minimum: 1 }) - @IsInt() - id: number; - - @ApiProperty() - @IsDate() - updated: Date; - - @ApiProperty() - @IsDate() - created: Date; -} diff --git a/src/modules/_mixin/dto/error.dto.ts b/src/modules/_mixin/dto/error.dto.ts deleted file mode 100644 index 4ec4858d..00000000 --- a/src/modules/_mixin/dto/error.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { IErrorResponseDTO } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsInt } from 'class-validator'; - -export class ErrorResponseDTO implements IErrorResponseDTO { - @ApiProperty({ example: 'Bad Request' }) - @IsString() - error: string; - - @ApiProperty({ required: false }) - @IsString() - message: string; - - @ApiProperty({ example: 400 }) - @IsInt() - statusCode: number; -} diff --git a/src/modules/_mixin/dto/input.dto.ts b/src/modules/_mixin/dto/input.dto.ts new file mode 100644 index 00000000..b6d1d107 --- /dev/null +++ b/src/modules/_mixin/dto/input.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { I18nIsId } from '@modules/_mixin/decorators'; + +export class InputIdParamDTO { + @ApiProperty({ minimum: 1 }) + @I18nIsId() + id: number; +} diff --git a/src/modules/_mixin/dto/message.dto.ts b/src/modules/_mixin/dto/message.dto.ts deleted file mode 100644 index b03c8af6..00000000 --- a/src/modules/_mixin/dto/message.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { IMessageResponseDTO } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, IsString } from 'class-validator'; - -/** - * Message response DTO class (used to send a message to the client) - * -> Mainly used for DELETE requests - * - * @example { message: 'User successfully deleted', statusCode: 200 } - */ -export class MessageResponseDTO implements IMessageResponseDTO { - @ApiProperty() - @IsString() - message: string; - - @ApiProperty({ example: 200 }) - @IsInt() - statusCode: number; -} diff --git a/src/modules/_mixin/dto/output.dto.ts b/src/modules/_mixin/dto/output.dto.ts new file mode 100644 index 00000000..f9063a5d --- /dev/null +++ b/src/modules/_mixin/dto/output.dto.ts @@ -0,0 +1,63 @@ +import type { + OutputErrorResponseDto, + OutputBaseDto, + OutputResponseDto, + HttpStatusNames, + I18nTranslations, +} from '#types/api'; + +import { PathImpl2 } from '@nestjs/config'; +import { ApiProperty } from '@nestjs/swagger'; +import { I18nContext, TranslateOptions } from 'nestjs-i18n'; + +/** + * Base read response DTO class (extended by all entity derived read DTOs) + */ +export abstract class OutputBaseDTO implements OutputBaseDto { + @ApiProperty({ minimum: 1 }) + id: number; + + @ApiProperty({ example: new Date().toISOString() }) + updated: Date; + + @ApiProperty({ example: new Date().toISOString() }) + created: Date; +} + +export class OutputMessageDTO implements OutputResponseDto { + constructor(key: PathImpl2, args?: TranslateOptions['args']) { + this.message = I18nContext.current().t(key, { args }); + } + + @ApiProperty() + message: string; + + @ApiProperty({ example: 200 }) + statusCode: number = 200; +} + +export class OutputCreatedDTO implements OutputResponseDto { + constructor(key: PathImpl2, args?: TranslateOptions['args']) { + this.message = I18nContext.current().t(key, { args }); + } + + @ApiProperty() + message: string; + + @ApiProperty({ example: 201 }) + statusCode: number = 201; +} + +/** + * Error response DTO class (used to send an error to the client) + */ +export class OutputErrorDTO implements OutputErrorResponseDto { + @ApiProperty() + errors: string[]; + + @ApiProperty({ example: 'Bad Request' }) + message: HttpStatusNames; + + @ApiProperty({ example: 500 }) + statusCode: number; +} diff --git a/src/modules/_mixin/entities/base.entity.ts b/src/modules/_mixin/entities/base.entity.ts index c0f2c68a..a200ce54 100644 --- a/src/modules/_mixin/entities/base.entity.ts +++ b/src/modules/_mixin/entities/base.entity.ts @@ -1,23 +1,19 @@ -import type { IBaseResponseDTO } from '#types/api'; +import type { OutputBaseDto } from '#types/api'; import { BaseEntity as BE, Entity, PrimaryKey, Property } from '@mikro-orm/core'; -import { ApiProperty } from '@nestjs/swagger'; /** * Base entity used for all entities, * - Contains the primary key, the creation and update dates */ @Entity({ abstract: true }) -export abstract class BaseEntity extends BE { +export abstract class BaseEntity extends BE { @PrimaryKey() - @ApiProperty({ minimum: 1 }) id: number; @Property({ type: Date, onCreate: () => new Date() }) - @ApiProperty({ type: Date }) created: Date; @Property({ type: Date, onCreate: () => new Date(), onUpdate: () => new Date() }) - @ApiProperty({ type: Date }) updated: Date; } diff --git a/src/modules/_mixin/http-errors/bad-request.ts b/src/modules/_mixin/http-errors/bad-request.ts new file mode 100644 index 00000000..8259ef48 --- /dev/null +++ b/src/modules/_mixin/http-errors/bad-request.ts @@ -0,0 +1,8 @@ +import type { HttpStatusNames } from '#types/api'; + +import { I18nHttpException } from './base'; + +export class i18nBadRequestException extends I18nHttpException { + override statusCode: number = 400; + override message: HttpStatusNames = 'Bad Request'; +} diff --git a/src/modules/_mixin/http-errors/base.ts b/src/modules/_mixin/http-errors/base.ts new file mode 100644 index 00000000..7437ca8d --- /dev/null +++ b/src/modules/_mixin/http-errors/base.ts @@ -0,0 +1,89 @@ +import type { I18nTranslations, OutputErrorResponseDto } from '#types/api'; + +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; +import { PathImpl2 } from '@nestjs/config'; +import { Response } from 'express'; +import { I18nContext, TranslateOptions } from 'nestjs-i18n'; + +export abstract class I18nHttpException extends Error implements OutputErrorResponseDto { + /** + * Pass a single key and optional args to translate the message + * @param key A key from the translation file, e.g. 'validations.user.success.registered' + * @param args An optional object containing the args to pass to the translation function + */ + constructor(key: PathImpl2, args?: TranslateOptions['args']); + + /** + * Pass multiple keys (with or without optional args) to be translated and concatenated + * + * You can use only keys: + * @example new OutputMessageDTO(['key.foo.bar', 'another.key']) + * + * Or specify args for some keys: + * @example new OutputMessageDTO(['key.foo.bar', { key: 'another.key', args: { test: 'value' } }]) + */ + constructor( + keys: ({ key: PathImpl2; args?: TranslateOptions['args'] } | PathImpl2)[], + ); + + /** + * @remark do not use this constructor directly, use the overloads instead + */ + constructor(...val: unknown[]) { + super(); + + const ctx = I18nContext.current(); + this.errors = []; + + if (val.length === 1 && Array.isArray(val[0])) { + // Handle the case where multiple keys (with or without optional args) are passed + if (val[0].length > 0) { + const keys = val[0] as ( + | { key: PathImpl2; args?: TranslateOptions['args'] } + | PathImpl2 + )[]; + + for (const key_or_key_with_args of keys) { + // if key_or_key_with_args is a string -> use it as key and args as undefined + if (typeof key_or_key_with_args === 'string') this.errors.push(ctx.t(key_or_key_with_args)); + // else -> use key and args to translate the message + else this.errors.push(ctx.t(key_or_key_with_args.key, { args: key_or_key_with_args.args })); + } + } + } else if (val.length === 1 && !Array.isArray(val[0])) { + // Handle the case where a single key is passed without args + const key = val[0] as PathImpl2; + + this.errors.push(ctx.t(key)); + } else if (val.length === 2) { + // Handle the case where a single key and optional args are passed + const key = val[0] as PathImpl2; + const args = val[1] as TranslateOptions['args']; + + this.errors.push(ctx.t(key, { args })); + } else { + /* istanbul ignore next */ + throw new Error('Invalid constructor arguments'); + } + } + + errors: string[]; + + abstract override message: string; + abstract statusCode: number; +} + +/** + * Exception filter to make NestJS understand that I18nHttpException behave the same as HttpException does + * @see https://docs.nestjs.com/exception-filters#binding-filters + */ +@Catch(I18nHttpException) +export class I18nHttpExceptionFilter implements ExceptionFilter { + catch(exception: I18nHttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const status = exception.statusCode; + + response.status(status).json(exception); + } +} diff --git a/src/modules/_mixin/http-errors/forbidden.ts b/src/modules/_mixin/http-errors/forbidden.ts new file mode 100644 index 00000000..8a8b95ac --- /dev/null +++ b/src/modules/_mixin/http-errors/forbidden.ts @@ -0,0 +1,8 @@ +import type { HttpStatusNames } from '#types/api'; + +import { I18nHttpException } from './base'; + +export class i18nForbiddenException extends I18nHttpException { + override statusCode: number = 403; + override message: HttpStatusNames = 'Forbidden'; +} diff --git a/src/modules/_mixin/http-errors/index.ts b/src/modules/_mixin/http-errors/index.ts new file mode 100644 index 00000000..64bfa36b --- /dev/null +++ b/src/modules/_mixin/http-errors/index.ts @@ -0,0 +1,5 @@ +export * from './forbidden'; +export * from './not-found'; +export * from './unauthorized'; +export * from './bad-request'; +export * from './base'; diff --git a/src/modules/_mixin/http-errors/not-found.ts b/src/modules/_mixin/http-errors/not-found.ts new file mode 100644 index 00000000..88b895df --- /dev/null +++ b/src/modules/_mixin/http-errors/not-found.ts @@ -0,0 +1,8 @@ +import type { HttpStatusNames } from '#types/api'; + +import { I18nHttpException } from './base'; + +export class i18nNotFoundException extends I18nHttpException { + override statusCode: number = 404; + override message: HttpStatusNames = 'Not Found'; +} diff --git a/src/modules/_mixin/http-errors/unauthorized.ts b/src/modules/_mixin/http-errors/unauthorized.ts new file mode 100644 index 00000000..0aee36bd --- /dev/null +++ b/src/modules/_mixin/http-errors/unauthorized.ts @@ -0,0 +1,8 @@ +import type { HttpStatusNames } from '#types/api'; + +import { I18nHttpException } from './base'; + +export class i18nUnauthorizedException extends I18nHttpException { + override statusCode: number = 401; + override message: HttpStatusNames = 'Unauthorized'; +} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index ad90d872..1276b3e7 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,68 +1,39 @@ import { Controller, Post, Body, Param, Get } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { z } from 'zod'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; -import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; -import { TranslateService } from '@modules/translate/translate.service'; -import { User } from '@modules/users/entities/user.entity'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; +import { OutputCreatedDTO, OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; import { UsersDataService } from '@modules/users/services/users-data.service'; -import { validate } from '@utils/validate'; import { AuthService } from './auth.service'; -import { TokenDTO } from './dto/get.dto'; -import { UserPostDTO, SignInDTO } from './dto/post.dto'; +import { InputEmailParamsDTO, InputRegisterUserDTO, InputSignInDTO } from './dto/input.dto'; +import { OutputTokenDTO } from './dto/output.dto'; @ApiTags('Authentification') @Controller('auth') export class AuthController { - constructor( - private readonly t: TranslateService, - private readonly authService: AuthService, - private readonly userService: UsersDataService, - ) {} + constructor(private readonly authService: AuthService, private readonly userService: UsersDataService) {} @Post('login') @ApiOperation({ summary: 'Sign in a user with email and password' }) - @ApiOkResponse({ description: 'OK', type: TokenDTO }) + @ApiOkResponse({ description: 'OK', type: OutputTokenDTO }) @ApiNotOkResponses({ 400: 'Bad Request, invalid fields', 401: 'Unauthorized, password mismatch', 403: 'Forbidden, email not verified', 404: 'User not found', }) - async login(@Body() signInDto: SignInDTO): Promise { - const schema = z - .object({ - password: z.string(), - email: z.string().email(), - }) - .strict(); - - validate(schema, signInDto); - + async login(@Body() signInDto: InputSignInDTO): Promise { return this.authService.signIn(signInDto.email, signInDto.password); } @Post('register') @ApiOperation({ summary: 'Register a new user' }) - @ApiOkResponse({ description: 'User created', type: MessageResponseDTO }) + @ApiOkResponse({ description: 'User created', type: OutputCreatedDTO }) @ApiNotOkResponses({ 400: 'Bad request, invalid fields', }) - async register(@Body() registerDto: UserPostDTO): Promise { - const schema = z - .object({ - password: z.string(), - first_name: z.string(), - last_name: z.string(), - email: z.string().email(), - birth_date: z.string().datetime(), - }) - .strict(); - - validate(schema, registerDto); - + async register(@Body() registerDto: InputRegisterUserDTO): Promise { return this.userService.register(registerDto); } @@ -70,16 +41,13 @@ export class AuthController { @ApiOperation({ summary: 'Validate a user account and redirect after' }) @ApiParam({ name: 'user_id', description: 'The user ID' }) @ApiParam({ name: 'token', description: 'The email verification token' }) - @ApiOkResponse({ description: 'OK', type: MessageResponseDTO }) + @ApiOkResponse({ description: 'OK', type: OutputMessageDTO }) @ApiNotOkResponses({ 400: 'Missing ID/token or email already verified', 401: 'Unauthorized, invalid token', 404: 'User not found', }) - async verifyEmailAndRedirect(@Param('user_id') user_id: number, @Param('token') token: string) { - validate(z.coerce.number().int().min(1), user_id, this.t.Errors.Id.Invalid(User, user_id)); - validate(z.string().min(12), token, this.t.Errors.JWT.Invalid()); - - return this.userService.verifyEmail(user_id, token); + async verifyEmailAndRedirect(@Param() params: InputEmailParamsDTO) { + return this.userService.verifyEmail(params.user_id, params.token); } } diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 2bf712c2..46aae104 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -3,7 +3,6 @@ import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { env } from '@env'; -import { TranslateService } from '@modules/translate/translate.service'; import { UsersModule } from '@modules/users/users.module'; import { AuthController } from './auth.controller'; @@ -22,7 +21,7 @@ import { JwtStrategy } from './strategies/jwt.strategy'; UsersModule, ], controllers: [AuthController], - providers: [AuthService, JwtStrategy, TranslateService], + providers: [AuthService, JwtStrategy], exports: [AuthService], }) export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 60b371bb..aa823d36 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -2,43 +2,35 @@ import type { email as Email } from '#types'; import type { JWTPayload } from '#types/api'; import { CreateRequestContext, MikroORM } from '@mikro-orm/core'; -import { ForbiddenException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { compareSync } from 'bcrypt'; import { env } from '@env'; -import { TranslateService } from '@modules/translate/translate.service'; +import { i18nForbiddenException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; import { User } from '@modules/users/entities/user.entity'; -import { TokenDTO } from './dto/get.dto'; +import { OutputTokenDTO } from './dto/output.dto'; @Injectable() export class AuthService { - constructor( - private readonly t: TranslateService, - private readonly orm: MikroORM, - private readonly jwtService: JwtService, - ) {} + constructor(private readonly orm: MikroORM, private readonly jwtService: JwtService) {} /** * Sign in a user and return a JWT token and the user ID * @param {Email} email the user email * @param {string} pass the user password (hashed or not) @default false - * @returns {Promise} The JWT token and the user ID + * @returns {Promise} The JWT token and the user ID */ @CreateRequestContext() - async signIn(email: Email, pass: string): Promise { + async signIn(email: Email, pass: string): Promise { const user: User = await this.orm.em.findOne(User, { email }, { fields: ['*', 'password'] }); + if (!user) throw new i18nNotFoundException('validations.user.not_found.email', { email }); - if (!user) throw new NotFoundException(this.t.Errors.Email.NotFound(User, email)); + if (user.password !== pass && !compareSync(pass, user.password)) + throw new i18nUnauthorizedException('validations.password.invalid.mismatch'); - if (user.password !== pass && !compareSync(pass, user.password)) { - throw new UnauthorizedException(this.t.Errors.Password.Mismatch()); - } - - if (!user.verified) { - throw new ForbiddenException(this.t.Errors.Email.NotVerified(User)); - } + if (!user.verified) throw new i18nForbiddenException('validations.user.unverified'); const payload = { sub: user.id, email: user.email }; return { @@ -50,15 +42,16 @@ export class AuthService { /** * Validate the user from the payload * @param {JWTPayload} payload JWT Payload to validate - * @returns {User} The user if found and valid, throw otherwise (email not verified) + * @returns {User} The user if found and valid, throw otherwise (account not verified) */ @CreateRequestContext() async validateUser(payload: JWTPayload): Promise { const user = await this.orm.em.findOne(User, { id: payload.sub }); // throw if user not verified - if (!user.email_verified) throw new UnauthorizedException(this.t.Errors.Email.NotVerified(User)); - + // -> should not happen as the JWT is provided by this.signIn method + /* istanbul ignore next-line */ + if (!user.verified) throw new i18nForbiddenException('validations.user.unverified'); return user; } @@ -67,7 +60,7 @@ export class AuthService { * @param {string} token token to verify * @returns {JWTPayload} The payload if valid * - * @throws {UnauthorizedException} If the token is invalid or expired + * @throws {i18nUnauthorizedException} If the token is invalid or expired */ verifyJWT(token: string): JWTPayload | never { const bearer = token.replace('Bearer', '').trim(); @@ -76,11 +69,11 @@ export class AuthService { return this.jwtService.verify(bearer, { secret: env.JWT_KEY }); } catch (err) { const error = err as Error; - if (error.name === 'TokenExpiredError') throw new UnauthorizedException(this.t.Errors.JWT.Expired()); - if (error.name === 'JsonWebTokenError') throw new UnauthorizedException(this.t.Errors.JWT.Invalid()); + if (error.name === 'TokenExpiredError') throw new i18nUnauthorizedException('validations.token.invalid.expired'); + if (error.name === 'JsonWebTokenError') throw new i18nUnauthorizedException('validations.token.invalid.format'); /* istanbul ignore next-line */ - throw new UnauthorizedException(this.t.Errors.JWT.Unknown()); + throw new i18nUnauthorizedException('validations.token.invalid.unknown'); } } } diff --git a/src/modules/auth/decorators/self-or-sub-perms.decorator.ts b/src/modules/auth/decorators/self-or-sub-perms.decorator.ts deleted file mode 100644 index 711d85e4..00000000 --- a/src/modules/auth/decorators/self-or-sub-perms.decorator.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { PERMISSION_NAMES } from '#types/api'; - -import { applyDecorators } from '@nestjs/common'; - -import { GuardPermissions } from './permissions.decorator'; -import { GuardSelfOrSubscribed } from './self-or-subscribed.decorator'; - -/** - * Set up the name of the parameter that contains the user id concerned by the route - * and the permissions that the user must have to access the route - * @param {string} param The name of the parameter that contains the user id - * @param {PERMISSION_NAMES[]} permissions The permissions that the user must have to access the route - */ -export const GuardSelfOrPermsOrSub = (param: string, permissions: PERMISSION_NAMES[]) => - applyDecorators(GuardSelfOrSubscribed(param), GuardPermissions(...permissions)); diff --git a/src/modules/auth/decorators/self-or-subscribed.decorator.ts b/src/modules/auth/decorators/self-or-subscribed.decorator.ts deleted file mode 100644 index ead29459..00000000 --- a/src/modules/auth/decorators/self-or-subscribed.decorator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { SetMetadata } from '@nestjs/common'; - -/** - * Set up the name of the parameter that contains the user id concerned by the route - * @param {string} param The name of the parameter that contains the user id - * - * TODO: (KEY: 2) Make a PR to implement subscriptions in the API - */ -export const GuardSelfOrSubscribed = (param: string) => SetMetadata('guard_self_param_key', param); diff --git a/src/modules/auth/dto/input.dto.ts b/src/modules/auth/dto/input.dto.ts new file mode 100644 index 00000000..1571212c --- /dev/null +++ b/src/modules/auth/dto/input.dto.ts @@ -0,0 +1,73 @@ +import type { email } from '#types'; +import type { + InputSignInDto, + InputRegisterUserAdminDto, + InputRegisterUserDto, + I18nTranslations, + InputRegisterUsersAdminDto, +} from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { ArrayNotEmpty, ArrayUnique, ValidateNested } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +import { I18nIsId, I18nIsEmail, I18nIsDate, I18nIsString, I18nIsStrongPassword } from '@modules/_mixin/decorators'; + +export class InputRegisterUserAdminDTO implements InputRegisterUserAdminDto { + @ApiProperty({ example: 'example@domain.com' }) + @I18nIsEmail() + email: email; + + @ApiProperty({ example: new Date('2001-01-01').toISOString() }) + @I18nIsDate() + birth_date: Date; + + @ApiProperty({ example: 'John' }) + @I18nIsString() + first_name: string; + + @ApiProperty({ example: 'Doe' }) + @I18nIsString() + last_name: string; +} + +export class InputRegisterUsersAdminDTO implements InputRegisterUsersAdminDto { + @ApiProperty({ type: InputRegisterUserAdminDTO, isArray: true }) + @ArrayNotEmpty({ message: i18nValidationMessage('validations.array.invalid.not_empty') }) + @ArrayUnique({ message: i18nValidationMessage('validations.array.invalid.duplicate') }) + @ValidateNested({ + each: true, + message: i18nValidationMessage('validations.array.invalid.format', { + type: InputRegisterUserAdminDTO.name, + }), + }) + @Type(() => InputRegisterUserAdminDTO) + users: InputRegisterUserAdminDTO[]; +} + +export class InputRegisterUserDTO extends InputRegisterUserAdminDTO implements InputRegisterUserDto { + @ApiProperty({ example: 'password' }) + @I18nIsStrongPassword() + password: string; +} + +export class InputSignInDTO implements InputSignInDto { + @ApiProperty({ type: String }) + @I18nIsEmail() + email: email; + + @ApiProperty({ example: 'password' }) + @I18nIsStrongPassword() + password: string; +} + +export class InputEmailParamsDTO { + @ApiProperty() + @I18nIsString() // Because it's a 12 chars password not a JWT token + token: string; + + @ApiProperty() + @I18nIsId() + user_id: number; +} diff --git a/src/modules/auth/dto/get.dto.ts b/src/modules/auth/dto/output.dto.ts similarity index 50% rename from src/modules/auth/dto/get.dto.ts rename to src/modules/auth/dto/output.dto.ts index ffc5e07f..ba779838 100644 --- a/src/modules/auth/dto/get.dto.ts +++ b/src/modules/auth/dto/output.dto.ts @@ -1,14 +1,11 @@ -import type { ITokenDTO } from '#types/api'; +import type { OutputTokenDto } from '#types/api'; import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, IsString } from 'class-validator'; -export class TokenDTO implements ITokenDTO { +export class OutputTokenDTO implements OutputTokenDto { @ApiProperty({ example: 'xxxxx.yyyyy.zzzzz' }) - @IsString() token: string; @ApiProperty({ minimum: 1 }) - @IsInt() user_id: number; } diff --git a/src/modules/auth/dto/post.dto.ts b/src/modules/auth/dto/post.dto.ts deleted file mode 100644 index 8f734352..00000000 --- a/src/modules/auth/dto/post.dto.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { email } from '#types'; -import type { ICreateUserDTO, ICreateUserByAdminDTO, ISignInDTO } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsDate, IsEmail, IsString, IsStrongPassword } from 'class-validator'; - -export class CreateUserDTO implements ICreateUserDTO { - @ApiProperty({ example: 'example@domain.com' }) - @IsEmail() - email: email; - - @ApiProperty({ example: new Date('2001-01-01').toISOString() }) - @IsDate() - birth_date: Date; - - @ApiProperty({ example: 'John' }) - @IsString() - first_name: string; - - @ApiProperty({ example: 'Doe' }) - @IsString() - last_name: string; -} - -export class UserPostDTO extends CreateUserDTO implements ICreateUserByAdminDTO { - @ApiProperty({ example: 'password' }) - @IsStrongPassword() - password: string; -} - -export class SignInDTO implements ISignInDTO { - @ApiProperty({ type: String }) - @IsString() - email: email; - - @ApiProperty({ example: 'password' }) - @IsString() - password: string; -} diff --git a/src/modules/auth/guards/self-or-perms.guard.ts b/src/modules/auth/guards/self-or-perms.guard.ts index 1ad1b014..68d54052 100644 --- a/src/modules/auth/guards/self-or-perms.guard.ts +++ b/src/modules/auth/guards/self-or-perms.guard.ts @@ -2,7 +2,6 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; -import { TranslateService } from '@modules/translate/translate.service'; import { UsersDataService } from '@modules/users/services/users-data.service'; import { PermissionGuard } from './permission.guard'; @@ -12,7 +11,6 @@ import { AuthService } from '../auth.service'; @Injectable() export class SelfOrPermissionGuard extends PermissionGuard implements CanActivate { constructor( - readonly t: TranslateService, override readonly jwtService: JwtService, override readonly userService: UsersDataService, override readonly reflector: Reflector, @@ -22,6 +20,6 @@ export class SelfOrPermissionGuard extends PermissionGuard implements CanActivat } override async canActivate(context: ExecutionContext) { - return SelfGuard.checkSelf(context, this.reflector, this.authService, this.t) || super.canActivate(context); + return SelfGuard.checkSelf(context, this.reflector, this.authService) || super.canActivate(context); } } diff --git a/src/modules/auth/guards/self-or-sub-or-perms.guard.ts b/src/modules/auth/guards/self-or-sub-or-perms.guard.ts deleted file mode 100644 index a44937e6..00000000 --- a/src/modules/auth/guards/self-or-sub-or-perms.guard.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CanActivate, ExecutionContext } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { JwtService } from '@nestjs/jwt'; - -import { TranslateService } from '@modules/translate/translate.service'; -import { UsersDataService } from '@modules/users/services/users-data.service'; - -import { SelfOrPermissionGuard } from './self-or-perms.guard'; -import { SubscribedGuard } from './subscribed.guard'; -import { AuthService } from '../auth.service'; - -export class SelfOrPermsOrSubGuard extends SelfOrPermissionGuard implements CanActivate { - constructor( - override readonly t: TranslateService, - override readonly jwtService: JwtService, - override readonly userService: UsersDataService, - override readonly reflector: Reflector, - override readonly authService: AuthService, - ) { - super(t, jwtService, userService, reflector, authService); - } - - override async canActivate(context: ExecutionContext) { - return ( - (await SubscribedGuard.checkSubscribed(context, this.authService, this.userService)) || - (await super.canActivate(context)) - ); - } -} diff --git a/src/modules/auth/guards/self-or-subscribed.guard.ts b/src/modules/auth/guards/self-or-subscribed.guard.ts deleted file mode 100644 index 85d8c05c..00000000 --- a/src/modules/auth/guards/self-or-subscribed.guard.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CanActivate, ExecutionContext } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { JwtService } from '@nestjs/jwt'; - -import { TranslateService } from '@modules/translate/translate.service'; -import { UsersDataService } from '@modules/users/services/users-data.service'; - -import { SelfGuard } from './self.guard'; -import { SubscribedGuard } from './subscribed.guard'; -import { AuthService } from '../auth.service'; - -export class SelfOrSubscribedGuard extends SubscribedGuard implements CanActivate { - constructor( - private readonly t: TranslateService, - override readonly jwtService: JwtService, - override readonly userService: UsersDataService, - override readonly reflector: Reflector, - override readonly authService: AuthService, - ) { - super(jwtService, userService, reflector, authService); - } - - override async canActivate(context: ExecutionContext) { - return SelfGuard.checkSelf(context, this.reflector, this.authService, this.t) || super.canActivate(context); - } -} diff --git a/src/modules/auth/guards/self.guard.ts b/src/modules/auth/guards/self.guard.ts index 1b7d6f4e..f7ff8905 100644 --- a/src/modules/auth/guards/self.guard.ts +++ b/src/modules/auth/guards/self.guard.ts @@ -4,9 +4,7 @@ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { z } from 'zod'; -import { TranslateService } from '@modules/translate/translate.service'; -import { User } from '@modules/users/entities/user.entity'; -import { validate } from '@utils/validate'; +import { i18nBadRequestException } from '@modules/_mixin/http-errors'; import { AuthService } from '../auth.service'; @@ -16,28 +14,19 @@ import { AuthService } from '../auth.service'; * **The user id parameter should be called `user_id`** * @example * UseGuards(SelfGuard) - * async route(@param('user_id') user_id: string) { + * async route(@param('user_id', ParseIntPipe) user_id: string) { * // ... * } */ @Injectable() export class SelfGuard implements CanActivate { - constructor( - private readonly t: TranslateService, - private readonly reflector: Reflector, - private readonly authService: AuthService, - ) {} + constructor(private readonly reflector: Reflector, private readonly authService: AuthService) {} canActivate(context: ExecutionContext): boolean | Promise | Observable { - return SelfGuard.checkSelf(context, this.reflector, this.authService, this.t); + return SelfGuard.checkSelf(context, this.reflector, this.authService); } - static checkSelf( - context: ExecutionContext, - reflector: Reflector, - authService: AuthService, - t: TranslateService, - ): boolean { + static checkSelf(context: ExecutionContext, reflector: Reflector, authService: AuthService): boolean { type req = Omit & { params: Record; body: Record | Array>; @@ -62,14 +51,30 @@ export class SelfGuard implements CanActivate { /* istanbul ignore next-line */ if (!id_key_values.length) return false; - /* istanbul ignore next-line */ - validate( - z.array(z.coerce.number().int().min(1)).min(1), - id_key_values, - id_key_values.length > 1 - ? t.Errors.Id.Invalids(User, id_key_values) - : t.Errors.Id.Invalid(User, id_key_values[0]), - ); + + try { + z.array( + z.coerce + .number() + .int() + .min(1) + .refine((val: unknown) => typeof val === 'number' && Number.isFinite(val)), + ) + .min(1) + .parse(id_key_values); + } catch { + if (id_key_values.length > 1) + /* istanbul ignore next-line */ + throw new i18nBadRequestException('validations.ids.invalid.format', { + property: id_key, + value: id_key_values.join("', '"), + }); + else + throw new i18nBadRequestException('validations.id.invalid.format', { + property: id_key, + value: id_key_values[0], + }); + } // Retrieve the authenticated user from the request's user object or session const token = request.headers.authorization; diff --git a/src/modules/auth/guards/subscribed.guard.ts b/src/modules/auth/guards/subscribed.guard.ts deleted file mode 100644 index 5c168b03..00000000 --- a/src/modules/auth/guards/subscribed.guard.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* istanbul ignore file */ - -import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { JwtService } from '@nestjs/jwt'; - -import { UsersDataService } from '@modules/users/services/users-data.service'; - -import { AuthService } from '../auth.service'; - -@Injectable() -export class SubscribedGuard implements CanActivate { - constructor( - protected readonly jwtService: JwtService, - protected readonly userService: UsersDataService, - protected readonly reflector: Reflector, - protected readonly authService: AuthService, - ) {} - - async canActivate(context: ExecutionContext) { - return SubscribedGuard.checkSubscribed(context, this.authService, this.userService); - } - - static async checkSubscribed(context: ExecutionContext, authService: AuthService, userService: UsersDataService) { - type req = Request & { headers: { authorization: string } }; - - const request = context.switchToHttp().getRequest(); - - // Retrieve the authenticated user from the request's user object or session - const bearer_token = request.headers.authorization; - - // Verify and decode the JWT token to extract the user ID - const payload = authService.verifyJWT(bearer_token); - - // Get the user from the database - // If no user found -> thrown within the service - const user = await userService.findOne(payload.sub, false); - - // TODO: (KEY: 2) Make a PR to implement subscriptions in the API - return user.subscribed; - } -} diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts index c2f517b5..46d3c2df 100644 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -6,7 +6,6 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { z } from 'zod'; import { env } from '@env'; -import { validate } from '@utils/validate'; import { AuthService } from '../auth.service'; @@ -29,16 +28,14 @@ export class JwtStrategy extends PassportStrategy(Strategy) { * @param payload - The payload from the JWT */ async validate(payload: JWTPayload) { - const schema = z - .object({ - sub: z.number().int().min(1), - email: z.string().email(), - iat: z.number().int().min(0), - exp: z.number().int().min(0), - }) - .strict(); - - validate(schema, payload); + z.object({ + sub: z.number().int().min(1), + email: z.string().email(), + iat: z.number().int().min(0), + exp: z.number().int().min(0), + }) + .strict() + .parse(payload); // Find the user from the payload // > If the user is not found, throw an error (Not found) diff --git a/src/modules/emails/emails.module.ts b/src/modules/emails/emails.module.ts index 972c2eaf..fe41e98a 100644 --- a/src/modules/emails/emails.module.ts +++ b/src/modules/emails/emails.module.ts @@ -1,12 +1,10 @@ import { Module } from '@nestjs/common'; -import { TranslateService } from '@modules/translate/translate.service'; - import { EmailsService } from './emails.service'; @Module({ imports: [], - providers: [EmailsService, TranslateService], + providers: [EmailsService], exports: [EmailsService], }) export class EmailsModule {} diff --git a/src/modules/emails/emails.service.ts b/src/modules/emails/emails.service.ts index 86485b37..4cac507f 100644 --- a/src/modules/emails/emails.service.ts +++ b/src/modules/emails/emails.service.ts @@ -3,11 +3,11 @@ import type { email } from '#types'; -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Transporter, createTransport } from 'nodemailer'; import { env } from '@env'; -import { TranslateService } from '@modules/translate/translate.service'; +import { i18nBadRequestException } from '@modules/_mixin/http-errors'; interface EmailOptions { to: string[]; @@ -20,7 +20,7 @@ interface EmailOptions { export class EmailsService { readonly transporter?: Transporter; - constructor(private readonly t: TranslateService) { + constructor() { this.transporter = env.EMAIL_ENABLED === false ? undefined @@ -36,17 +36,9 @@ export class EmailsService { } /** - * Check if an email is allowed to be used (to register for example) and if it is valid - * @see https://emailregex.com/ - * - * @param email + * Determine if an email is blacklisted or not */ - validateEmail(email: email): void | never { - if (typeof email !== 'string' || !email.includes('@')) - throw new BadRequestException(this.t.Errors.Email.Invalid(email)); - - if (email.length > 60 || email.length < 6) throw new BadRequestException(this.t.Errors.Email.Malformed(email)); - + static isEmailBlacklisted(email: email): boolean { const whitelisted = { hosts: env.WHITELISTED_HOSTS.split(';'), emails: env.WHITELISTED_EMAILS.split(';'), @@ -57,14 +49,33 @@ export class EmailsService { emails: env.BLACKLISTED_EMAILS.split(';'), }; - if (whitelisted.hosts.some((host) => email.endsWith(host)) || whitelisted.emails.includes(email)) return; - if (blacklisted.hosts.some((host) => email.endsWith(host)) || blacklisted.emails.includes(email)) - throw new BadRequestException(this.t.Errors.Email.Blacklisted(email)); + if (whitelisted.hosts.some((host) => email.endsWith(host)) || whitelisted.emails.includes(email)) return false; + if (blacklisted.hosts.some((host) => email.endsWith(host)) || blacklisted.emails.includes(email)) return true; + + return false; + } + + /** + * Check if an email is allowed to be used (to register for example) and if it is valid + * @see https://emailregex.com/ + * + * @param email + */ + validateEmail(email: email): void | never { + if (typeof email !== 'string' || !email.includes('@')) + throw new i18nBadRequestException('validations.email.invalid.format', { property: 'email', value: email }); + + if (email.length > 60 || email.length < 6) + throw new i18nBadRequestException('validations.email.invalid.size', { email }); + + if (EmailsService.isEmailBlacklisted(email)) + throw new i18nBadRequestException('validations.email.invalid.blacklisted', { property: 'email', value: email }); const regex = new RegExp( /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/, ); - if (!regex.test(email)) throw new BadRequestException(this.t.Errors.Email.Invalid(email)); + if (!regex.test(email)) + throw new i18nBadRequestException('validations.email.invalid.format', { property: 'email', value: email }); } async sendEmail(options: EmailOptions) { diff --git a/src/modules/files/dto/get.dto.ts b/src/modules/files/dto/output.dto.ts similarity index 55% rename from src/modules/files/dto/get.dto.ts rename to src/modules/files/dto/output.dto.ts index c8684da0..0483b657 100644 --- a/src/modules/files/dto/get.dto.ts +++ b/src/modules/files/dto/output.dto.ts @@ -1,60 +1,46 @@ -import type { IFileGetDTO, IFileVisibilityGroupGetDTO } from '#types/api'; +import type { OutputFileDto, OutputFileVisibilityGroupDto } from '#types/api'; import { ApiProperty } from '@nestjs/swagger'; -import { IsDate, IsInt, IsString } from 'class-validator'; -export class FileGetDTO implements IFileGetDTO { +export class OutputFileDTO implements OutputFileDto { @ApiProperty() - @IsInt() id: number; @ApiProperty() - @IsDate() updated: Date; @ApiProperty() - @IsDate() created: Date; @ApiProperty() - @IsString() filename: string; @ApiProperty() - @IsString() mimetype: string; @ApiProperty() - @IsString() path: string; @ApiProperty() - @IsInt() size: number; @ApiProperty({ required: false }) - @IsInt() visibility?: number; @ApiProperty() - @IsString() description?: string; } -export class FileVisibilityGroupGetDTO implements IFileVisibilityGroupGetDTO { +export class OutputFileVisibilityGroupDTO implements OutputFileVisibilityGroupDto { @ApiProperty() - @IsString() // TODO : verify uppercase name: Uppercase; @ApiProperty() - @IsString() description: string; @ApiProperty() - @IsInt() users_count: number; @ApiProperty() - @IsInt() files_count: number; } diff --git a/src/modules/files/files.module.ts b/src/modules/files/files.module.ts index 4bf2fe66..07bd5d4d 100644 --- a/src/modules/files/files.module.ts +++ b/src/modules/files/files.module.ts @@ -2,7 +2,6 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { Module } from '@nestjs/common'; import { EmailsService } from '@modules/emails/emails.service'; -import { TranslateService } from '@modules/translate/translate.service'; import { UsersDataService } from '@modules/users/services/users-data.service'; import { FileVisibilityGroup } from './entities/file-visibility.entity'; @@ -11,7 +10,7 @@ import { ImagesService } from './images.service'; @Module({ imports: [MikroOrmModule.forFeature([FileVisibilityGroup])], - providers: [EmailsService, FilesService, TranslateService, ImagesService, UsersDataService], + providers: [EmailsService, FilesService, ImagesService, UsersDataService], exports: [FilesService], }) export class FilesModule {} diff --git a/src/modules/files/files.service.ts b/src/modules/files/files.service.ts index e276315d..c84559ba 100644 --- a/src/modules/files/files.service.ts +++ b/src/modules/files/files.service.ts @@ -1,58 +1,63 @@ -import type { aspect_ratio } from '#types'; - import { randomUUID } from 'crypto'; import { accessSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'; import { join } from 'path'; import { Readable } from 'stream'; import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable, StreamableFile } from '@nestjs/common'; import { fromBuffer, MimeType } from 'file-type'; -import { TranslateService } from '@modules/translate/translate.service'; +import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; import { User } from '@modules/users/entities/user.entity'; import { UsersDataService } from '@modules/users/services/users-data.service'; import { FileVisibilityGroup } from './entities/file-visibility.entity'; import { File } from './entities/file.entity'; -import { ImagesService } from './images.service'; -type WriteFileOptions = { +export type WriteFileOptions = { directory: string; filename: string; }; -type WriteImageOptions = WriteFileOptions & { - aspect_ratio: aspect_ratio; -}; - @Injectable() export class FilesService { - constructor( - private readonly orm: MikroORM, - private readonly t: TranslateService, - private readonly imagesService: ImagesService, - private readonly usersDataService: UsersDataService, - ) {} + constructor(private readonly orm: MikroORM, private readonly usersDataService: UsersDataService) {} + + /** + * Return a file as a StreamableFile + * @param {File} file the file to get as a stream + * @param {number} user_id the user id who wants to get the file + * + * @throws {i18nUnauthorizedException} if the user is not allowed to read the file + */ + async getAsStreamable(file: File, user_id: number): Promise { + if (!(await this.canReadFile(file, user_id))) + throw new i18nUnauthorizedException('validations.user.invalid.not_in_file_visibility_group', { + group_name: file.visibility?.name, + }); + + return new StreamableFile(this.toReadable(file)); + } /** * Determine if the given user can read the given file. - * @param {File} file - The file to check the visibility of. - * @param {User} user - The user to check the visibility for. + * @param {File} file - The file to check the visibility of. + * @param {number} userId - The user to check the visibility for. */ - async canReadFile(file: File, user: User): Promise { + async canReadFile(file: File, userId: number): Promise { // If the file has no visibility group, it's public await file.visibility?.init(); if (!file.visibility) return true; // File owner can always read his own files - if (file.owner instanceof User && file.owner.id === user.id) return true; + if (file.owner instanceof User && file.owner.id === userId) return true; // If user has ROOT / CAN_READ_FILE, he can read the file no matter what - if (await this.usersDataService.hasPermissionOrRoleWithPermission(user.id, false, ['CAN_READ_FILE'])) return true; + if (await this.usersDataService.hasPermissionOrRoleWithPermission(userId, false, ['CAN_READ_FILE'])) return true; // Check if the user has the correct visibility group to read the file - await user.files_visibility_groups.init(); + const user = await this.orm.em.findOne(User, { id: userId }, { populate: ['files_visibility_groups'] }); + return ( user.files_visibility_groups .getItems() @@ -61,18 +66,23 @@ export class FilesService { } /** - * Upload file on disk + * Write file on disk + * @param {Buffer} buffer The file buffer + * @param {WriteFileOptions} options The options to write the file + * @param {MimeType[]} allowedMimetype The allowed MIME types */ async writeOnDisk(buffer: Buffer, options: WriteFileOptions, allowedMimetype: MimeType[]) { - if (!buffer) throw new BadRequestException(this.t.Errors.File.NotProvided()); + if (!buffer) throw new i18nBadRequestException('validations.file.invalid.not_provided'); const fileType = await fromBuffer(buffer); /* istanbul ignore next-line */ - if (!fileType) throw new BadRequestException(this.t.Errors.File.UndefinedMimeType()); + if (!fileType || !fileType.mime) throw new i18nBadRequestException('validations.file.invalid.no_mime_type'); /* istanbul ignore next-line */ if (!allowedMimetype.includes(fileType.mime)) - throw new BadRequestException(this.t.Errors.File.InvalidMimeType(allowedMimetype)); + throw new i18nBadRequestException('validations.file.invalid.unauthorized_mime_type', { + mime_types: allowedMimetype.join("', '"), + }); const filename = `${options.filename}_${randomUUID()}.${fileType.ext}`; const filepath = join(options.directory, filename); @@ -81,7 +91,7 @@ export class FilesService { // Scan the file with an antivirus // TODO: (KEY: 5) Implement an antivirus (do a specific PR for it, as it's quite a big feature) /* istanbul ignore next-line */ - if (await this.scanWithAntivirus(buffer)) throw new BadRequestException(this.t.Errors.File.Infected(filename)); + if (await this.scanWithAntivirus(buffer)) throw new i18nBadRequestException('validations.file.invalid.infected'); // Write the file on disk mkdirSync(options.directory, { recursive: true }); @@ -96,29 +106,6 @@ export class FilesService { }; } - /** - * Upload file on disk, but convert it to webp first - * (unless it's a GIF or webp already) - * TODO: (KEY: 4) move this function to the images.service.ts file ? - */ - async writeOnDiskAsImage(file: Express.Multer.File, options: WriteImageOptions) { - if (!file) throw new BadRequestException(this.t.Errors.File.NotProvided()); - - let buffer = file.buffer; - - if (!file.mimetype.startsWith('image/')) - throw new BadRequestException(this.t.Errors.File.InvalidMimeType(['image/*'])); - - // Check if the file respect the aspect ratio - if (!(await this.imagesService.validateAspectRatio(buffer, options.aspect_ratio))) - throw new BadRequestException(this.t.Errors.Image.InvalidAspectRatio(options.aspect_ratio)); - - // Convert the file to webp (unless it's a GIF or already a webp) - buffer = await this.imagesService.convertToWebp(buffer); - - return this.writeOnDisk(buffer, options, ['image/webp', 'image/gif']); - } - /** * Scan a file with an Antivirus * TODO: (KEY: 5) Implement an antivirus (do a specific PR for it, as it's quite a big feature) @@ -138,7 +125,7 @@ export class FilesService { @CreateRequestContext() async getVisibilityGroup(name: Uppercase = 'SUBSCRIBER'): Promise { const res = await this.orm.em.findOne(FileVisibilityGroup, { name }, { populate: ['users', 'files'] }); - if (!res) throw new BadRequestException(this.t.Errors.Entity.NotFound(FileVisibilityGroup, name, 'name')); + if (!res) throw new i18nBadRequestException('validations.file_visibility_group.invalid.not_found', { name }); return res; } @@ -153,7 +140,9 @@ export class FilesService { accessSync(file.path); } catch { if (silent) return; - throw new NotFoundException(this.t.Errors.File.NotFoundOnDisk(file.filename)); + throw new i18nNotFoundException('validations.file.invalid.not_found', { + filename: file.filename, + }); } rmSync(file.path); @@ -168,7 +157,9 @@ export class FilesService { try { accessSync(file.path); } catch { - throw new NotFoundException(this.t.Errors.File.NotFoundOnDisk(file.filename)); + throw new i18nNotFoundException('validations.file.invalid.not_found', { + filename: file.filename, + }); } const readable = new Readable({ diff --git a/src/modules/files/images.service.ts b/src/modules/files/images.service.ts index c3f84d7c..7ed79022 100644 --- a/src/modules/files/images.service.ts +++ b/src/modules/files/images.service.ts @@ -1,10 +1,19 @@ import type { aspect_ratio } from '#types'; import { Injectable } from '@nestjs/common'; +import { fromBuffer } from 'file-type'; import sharp from 'sharp'; +import { i18nBadRequestException } from '@modules/_mixin/http-errors'; + +import { FilesService, WriteFileOptions } from './files.service'; + +type WriteImageOptions = WriteFileOptions & { + aspect_ratio: aspect_ratio; +}; + @Injectable() -export class ImagesService { +export class ImagesService extends FilesService { async validateAspectRatio(buffer: Buffer, aspect_ratio: aspect_ratio): Promise { const { width, height } = await sharp(buffer).metadata(); const [aspectWidth, aspectHeight] = aspect_ratio.split(':').map((s) => parseInt(s, 10)); @@ -19,4 +28,27 @@ export class ImagesService { // convert the image to webp otherwise return sharp(buffer).webp().toBuffer(); } + + /** + * Upload file on disk, but convert it to webp first + * (unless it's a GIF or webp already) + */ + override async writeOnDisk(buffer: Buffer, options: WriteImageOptions) { + if (!buffer) throw new i18nBadRequestException('validations.file.invalid.not_provided'); + + const fileType = await fromBuffer(buffer); + + if (!fileType || !fileType.mime.startsWith('image/')) + throw new i18nBadRequestException('validations.file.invalid.unauthorized_mime_type', { mime_types: 'image/*' }); + + // Check if the file respect the asked aspect ratio + if (!(await this.validateAspectRatio(buffer, options.aspect_ratio))) + throw new i18nBadRequestException('validations.image.invalid.aspect_ratio', { + aspect_ratio: options.aspect_ratio, + }); + + buffer = await this.convertToWebp(buffer); + + return super.writeOnDisk(buffer, options, ['image/webp', 'image/gif']); + } } diff --git a/src/modules/logs/dto/get.dto.ts b/src/modules/logs/dto/output.dto.ts similarity index 67% rename from src/modules/logs/dto/get.dto.ts rename to src/modules/logs/dto/output.dto.ts index 54123fc8..3b0daad4 100644 --- a/src/modules/logs/dto/get.dto.ts +++ b/src/modules/logs/dto/output.dto.ts @@ -1,62 +1,47 @@ -import type { ILogDTO } from '#types/api'; +import type { OutputLogDto } from '#types/api'; import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, IsString } from 'class-validator'; -export class LogDTO implements ILogDTO { +export class OutputLogDTO implements OutputLogDto { @ApiProperty() - @IsInt() user_id: number; @ApiProperty() - @IsString() action: string; @ApiProperty() - @IsString() ip: string; @ApiProperty() - @IsString() user_agent: string; @ApiProperty() - @IsString() route: string; @ApiProperty() - @IsString() method: string; @ApiProperty() - @IsString() body: string; @ApiProperty() - @IsString() query: string; @ApiProperty() - @IsString() params: string; @ApiProperty({ required: false }) - @IsString() response?: string; @ApiProperty({ required: false }) - @IsInt() status_code?: number; @ApiProperty({ required: false }) - @IsString() error?: string; @ApiProperty({ required: false }) - @IsString() error_stack?: string; @ApiProperty({ required: false }) - @IsString() error_message?: string; } diff --git a/src/modules/logs/logs.controller.ts b/src/modules/logs/logs.controller.ts index cfbbbb3f..dbc32da4 100644 --- a/src/modules/logs/logs.controller.ts +++ b/src/modules/logs/logs.controller.ts @@ -1,19 +1,16 @@ import { Controller, Delete, Get, Param, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiTags, ApiBearerAuth, ApiOkResponse, ApiParam, ApiOperation } from '@nestjs/swagger'; -import { z } from 'zod'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; -import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/_mixin/dto/input.dto'; +import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { SelfOrPermissionGuard } from '@modules/auth/guards/self-or-perms.guard'; -import { TranslateService } from '@modules/translate/translate.service'; -import { User } from '@modules/users/entities/user.entity'; -import { validate } from '@utils/validate'; -import { LogDTO } from './dto/get.dto'; +import { OutputLogDTO } from './dto/output.dto'; import { LogsService } from './logs.service'; @Controller('logs') @@ -21,31 +18,27 @@ import { LogsService } from './logs.service'; @ApiTags('Logs') @ApiBearerAuth() export class LogsController { - constructor(private readonly logsService: LogsService, private readonly t: TranslateService) {} + constructor(private readonly logsService: LogsService) {} - @Get('user/:user_id') + @Get('user/:id') @UseGuards(SelfOrPermissionGuard) - @GuardSelfOrPermissions('user_id', ['CAN_READ_LOGS_OF_USER']) + @GuardSelfOrPermissions('id', ['CAN_READ_LOGS_OF_USER']) @ApiOperation({ summary: 'Get all logs of a user' }) @ApiParam({ name: 'id', description: 'The user ID' }) - @ApiOkResponse({ description: 'User logs retrieved', type: [LogDTO] }) + @ApiOkResponse({ description: 'User logs retrieved', type: [OutputLogDTO] }) @ApiNotOkResponses({ 400: 'Invalid user ID', 404: 'User not found' }) - async getUserLogs(@Param('user_id') id: number): Promise { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - - return this.logsService.getUserLogs(id); + async getUserLogs(@Param() params: InputIdParamDTO): Promise { + return this.logsService.getUserLogs(params.id); } - @Delete('user/:user_id') + @Delete('user/:id') @UseGuards(PermissionGuard) @GuardPermissions('CAN_DELETE_LOGS_OF_USER') @ApiOperation({ summary: 'Delete all logs of a user' }) @ApiParam({ name: 'id', description: 'The user ID' }) - @ApiOkResponse({ description: 'User logs deleted', type: MessageResponseDTO }) + @ApiOkResponse({ description: 'User logs deleted', type: OutputMessageDTO }) @ApiNotOkResponses({ 400: 'Invalid user ID', 404: 'User not found' }) - async deleteUserLogs(@Param('user_id') id: number): Promise { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - - return this.logsService.deleteUserLogs(id); + async deleteUserLogs(@Param() params: InputIdParamDTO): Promise { + return this.logsService.deleteUserLogs(params.id); } } diff --git a/src/modules/logs/logs.module.ts b/src/modules/logs/logs.module.ts index 530f91e3..ed78a1a4 100644 --- a/src/modules/logs/logs.module.ts +++ b/src/modules/logs/logs.module.ts @@ -3,7 +3,6 @@ import { Module } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { AuthService } from '@modules/auth/auth.service'; -import { TranslateService } from '@modules/translate/translate.service'; import { UsersModule } from '@modules/users/users.module'; import { Log } from './entities/log.entity'; @@ -12,7 +11,7 @@ import { LogsService } from './logs.service'; @Module({ imports: [MikroOrmModule.forFeature([Log]), UsersModule], - providers: [LogsService, JwtService, AuthService, TranslateService], + providers: [LogsService, JwtService, AuthService], controllers: [LogsController], exports: [LogsService], }) diff --git a/src/modules/logs/logs.service.ts b/src/modules/logs/logs.service.ts index e7dbafa3..caa73a72 100644 --- a/src/modules/logs/logs.service.ts +++ b/src/modules/logs/logs.service.ts @@ -2,14 +2,14 @@ import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { TranslateService } from '@modules/translate/translate.service'; +import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; -import { LogDTO } from './dto/get.dto'; +import { OutputLogDTO } from './dto/output.dto'; import { Log } from './entities/log.entity'; @Injectable() export class LogsService { - constructor(private readonly orm: MikroORM, private readonly t: TranslateService) {} + constructor(private readonly orm: MikroORM) {} /** * Remove all logs that are older than 2 months each day at 7am @@ -20,12 +20,12 @@ export class LogsService { await this.orm.em.nativeDelete(Log, { created: { $lt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 60) } }); } - async getUserLogs(id: number): Promise { - return (await this.orm.em.find(Log, { user: id })).map((log) => log.toObject() as unknown as LogDTO); + async getUserLogs(id: number): Promise { + return (await this.orm.em.find(Log, { user: id })).map((log) => log.toObject() as unknown as OutputLogDTO); } - async deleteUserLogs(id: number) { + async deleteUserLogs(id: number): Promise { await this.orm.em.nativeDelete(Log, { user: id }); - return { message: this.t.Success.Entity.Deleted(Log), statusCode: 200 }; + return new OutputMessageDTO('validations.logs.success.deleted'); } } diff --git a/src/modules/permissions/dto/get.dto.ts b/src/modules/permissions/dto/get.dto.ts deleted file mode 100644 index f1010119..00000000 --- a/src/modules/permissions/dto/get.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { IPermissionGetDTO, IPermissionsOfRoleDTO, PERMISSION_NAMES } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, IsArray, IsDate, IsBoolean } from 'class-validator'; - -import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -import { BaseResponseDTO } from '@modules/_mixin/dto/base.dto'; - -export class PermissionsOfRoleDTO implements IPermissionsOfRoleDTO { - @ApiProperty() - @IsInt() - id: number; - - @ApiProperty({ enum: PERMISSIONS_NAMES, isArray: true }) - @IsArray() - permissions: PERMISSION_NAMES[]; -} - -export class PermissionGetDTO extends BaseResponseDTO implements IPermissionGetDTO { - @ApiProperty({ enum: PERMISSIONS_NAMES }) - @IsArray() - name: PERMISSION_NAMES; - - @ApiProperty() - @IsInt() - user_id: number; - - @ApiProperty() - @IsBoolean() - revoked: boolean; - - @ApiProperty() - @IsDate() - expires: Date; -} diff --git a/src/modules/permissions/dto/input.dto.ts b/src/modules/permissions/dto/input.dto.ts new file mode 100644 index 00000000..2ca8a9c8 --- /dev/null +++ b/src/modules/permissions/dto/input.dto.ts @@ -0,0 +1,57 @@ +import type { + InputUpdatePermissionDto, + PERMISSION_NAMES, + InputCreatePermissionDto, + I18nTranslations, +} from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsIn } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; +import { I18nIsDate, I18nIsId, I18nIsBoolean } from '@modules/_mixin/decorators'; + +export class InputCreatePermissionDTO implements InputCreatePermissionDto { + @ApiProperty() + @I18nIsId() + id: number; + + @ApiProperty({ enum: PERMISSIONS_NAMES }) + @IsIn(PERMISSIONS_NAMES, { + message: i18nValidationMessage('validations.permission.invalid.format', { + permissions: PERMISSIONS_NAMES.join("', '"), + }), + }) + permission: PERMISSION_NAMES; + + @ApiProperty() + @I18nIsDate() + expires: Date; +} + +export class InputUpdatePermissionDTO implements InputUpdatePermissionDto { + @ApiProperty({ required: true, minimum: 1 }) + @I18nIsId() + id: number; + + @ApiProperty({ required: true, minimum: 1 }) + @I18nIsId() + user_id: number; + + @ApiProperty({ enum: PERMISSIONS_NAMES }) + @IsIn(PERMISSIONS_NAMES, { + message: i18nValidationMessage('validations.permission.invalid.format', { + permissions: PERMISSIONS_NAMES.join("', '"), + }), + }) + name: PERMISSION_NAMES; + + @ApiProperty() + @I18nIsDate() + expires: Date; + + @ApiProperty() + @I18nIsBoolean() + revoked: boolean; +} diff --git a/src/modules/permissions/dto/output.dto.ts b/src/modules/permissions/dto/output.dto.ts new file mode 100644 index 00000000..d3ac3ef1 --- /dev/null +++ b/src/modules/permissions/dto/output.dto.ts @@ -0,0 +1,28 @@ +import type { OutputPermissionDto, OutputPermissionsOfRoleDto, PERMISSION_NAMES } from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; + +import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; +import { OutputBaseDTO } from '@modules/_mixin/dto/output.dto'; + +export class OutputPermissionsOfRoleDTO implements OutputPermissionsOfRoleDto { + @ApiProperty() + id: number; + + @ApiProperty({ enum: PERMISSIONS_NAMES, isArray: true }) + permissions: PERMISSION_NAMES[]; +} + +export class OutputPermissionDTO extends OutputBaseDTO implements OutputPermissionDto { + @ApiProperty({ enum: PERMISSIONS_NAMES }) + name: PERMISSION_NAMES; + + @ApiProperty() + user_id: number; + + @ApiProperty() + revoked: boolean; + + @ApiProperty() + expires: Date; +} diff --git a/src/modules/permissions/dto/patch.dto.ts b/src/modules/permissions/dto/patch.dto.ts deleted file mode 100644 index 3b5721a2..00000000 --- a/src/modules/permissions/dto/patch.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { PERMISSION_NAMES, IPermissionPatchDTO } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, IsString, IsDate, IsBoolean } from 'class-validator'; - -import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; - -export class PermissionPatchDTO implements IPermissionPatchDTO { - @ApiProperty({ required: true, minimum: 1 }) - @IsInt() - id: number; - - @ApiProperty({ required: true, minimum: 1 }) - @IsInt() - user_id: number; - - @ApiProperty({ enum: PERMISSIONS_NAMES }) - @IsString() - name: PERMISSION_NAMES; - - @ApiProperty() - @IsDate() - expires: Date; - - @ApiProperty() - @IsBoolean() - revoked: boolean; -} diff --git a/src/modules/permissions/dto/post.dto.ts b/src/modules/permissions/dto/post.dto.ts deleted file mode 100644 index 7d3ee494..00000000 --- a/src/modules/permissions/dto/post.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { PERMISSION_NAMES, PermissionPostDto } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsDate, IsInt, IsString } from 'class-validator'; - -import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; - -export class PermissionPostDTO implements PermissionPostDto { - @ApiProperty() - @IsInt() - id: number; - - @ApiProperty({ enum: PERMISSIONS_NAMES }) - @IsString() - permission: PERMISSION_NAMES; - - @ApiProperty() - @IsDate() - expires: Date; -} diff --git a/src/modules/permissions/permissions.controller.ts b/src/modules/permissions/permissions.controller.ts index 506c48f1..cb5026e9 100644 --- a/src/modules/permissions/permissions.controller.ts +++ b/src/modules/permissions/permissions.controller.ts @@ -1,20 +1,16 @@ import { Body, Controller, Get, Param, Post, UseGuards, Patch } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { z } from 'zod'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/_mixin/dto/input.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { SelfOrPermissionGuard } from '@modules/auth/guards/self-or-perms.guard'; -import { TranslateService } from '@modules/translate/translate.service'; -import { User } from '@modules/users/entities/user.entity'; -import { validate } from '@utils/validate'; -import { PermissionGetDTO } from './dto/get.dto'; -import { PermissionPatchDTO } from './dto/patch.dto'; -import { PermissionPostDTO } from './dto/post.dto'; +import { InputUpdatePermissionDTO, InputCreatePermissionDTO } from './dto/input.dto'; +import { OutputPermissionDTO } from './dto/output.dto'; import { PermissionsService } from './permissions.service'; @Controller('permissions') @@ -22,27 +18,18 @@ import { PermissionsService } from './permissions.service'; @ApiTags('Permissions') @ApiBearerAuth() export class PermissionsController { - constructor(private readonly permsService: PermissionsService, private readonly t: TranslateService) {} + constructor(private readonly permsService: PermissionsService) {} @Post() @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_PERMISSIONS_OF_USER') @ApiOperation({ summary: 'Add a permission to a user' }) - @ApiOkResponse({ description: 'The added permission', type: PermissionGetDTO }) + @ApiOkResponse({ description: 'The added permission', type: OutputPermissionDTO }) @ApiNotOkResponses({ 400: 'Bad request, invalid fields', 404: 'User not found', }) - async addToUser(@Body() body: PermissionPostDTO): Promise { - const schema = z - .object({ - expires: z.string().datetime(), - id: z.number().int().min(1).optional(), - permission: z.string(), - }) - .strict(); - validate(schema, body); - + async addToUser(@Body() body: InputCreatePermissionDTO): Promise { return this.permsService.addPermissionToUser(body); } @@ -50,36 +37,20 @@ export class PermissionsController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_PERMISSIONS_OF_USER') @ApiOperation({ summary: 'Edit permission of a user' }) - @ApiOkResponse({ description: 'The modified user permission', type: PermissionGetDTO }) + @ApiOkResponse({ description: 'The modified user permission', type: OutputPermissionDTO }) @ApiNotOkResponses({ 404: 'User/permission not found' }) - async editPermissionFromUser(@Body() body: PermissionPatchDTO): Promise { - const schema = z - .object({ - id: z.number().int().min(1), - expires: z.string().datetime().optional(), - revoked: z.boolean().optional(), - user_id: z.number().int().min(1).optional(), - name: z - .string() - .optional() - .refine((name) => name === name.toUpperCase(), {}), - }) - .strict(); - validate(schema, body); - + async editPermissionFromUser(@Body() body: InputUpdatePermissionDTO): Promise { return this.permsService.editPermissionOfUser(body); } - @Get(':user_id') + @Get(':id') @UseGuards(SelfOrPermissionGuard) - @GuardSelfOrPermissions('user_id', ['CAN_READ_PERMISSIONS_OF_USER']) + @GuardSelfOrPermissions('id', ['CAN_READ_PERMISSIONS_OF_USER']) @ApiOperation({ summary: 'Get all permissions of a user (active, revoked and expired)' }) - @ApiParam({ name: 'user_id', description: 'The user ID' }) - @ApiOkResponse({ description: 'User permission(s) retrieved', type: [PermissionGetDTO] }) + @ApiParam({ name: 'id', description: 'The user ID' }) + @ApiOkResponse({ description: 'User permission(s) retrieved', type: [OutputPermissionDTO] }) @ApiNotOkResponses({ 404: 'User not found' }) - async getUserPermissions(@Param('user_id') id: number): Promise { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - - return this.permsService.getPermissionsOfUser(id); + async getUserPermissions(@Param() params: InputIdParamDTO): Promise { + return this.permsService.getPermissionsOfUser(params.id); } } diff --git a/src/modules/permissions/permissions.module.ts b/src/modules/permissions/permissions.module.ts index d8eee0cd..0049d36c 100644 --- a/src/modules/permissions/permissions.module.ts +++ b/src/modules/permissions/permissions.module.ts @@ -3,7 +3,6 @@ import { Module } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { AuthService } from '@modules/auth/auth.service'; -import { TranslateService } from '@modules/translate/translate.service'; import { UsersModule } from '@modules/users/users.module'; import { Permission } from './entities/permission.entity'; @@ -12,7 +11,7 @@ import { PermissionsService } from './permissions.service'; @Module({ imports: [MikroOrmModule.forFeature([Permission]), UsersModule], - providers: [PermissionsService, JwtService, AuthService, TranslateService], + providers: [PermissionsService, JwtService, AuthService], controllers: [PermissionsController], exports: [PermissionsService], }) diff --git a/src/modules/permissions/permissions.service.ts b/src/modules/permissions/permissions.service.ts index aa1bab25..1c89a955 100644 --- a/src/modules/permissions/permissions.service.ts +++ b/src/modules/permissions/permissions.service.ts @@ -1,19 +1,17 @@ import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -import { TranslateService } from '@modules/translate/translate.service'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/_mixin/http-errors'; -import { PermissionGetDTO } from './dto/get.dto'; -import { PermissionPatchDTO } from './dto/patch.dto'; -import { PermissionPostDTO } from './dto/post.dto'; +import { InputUpdatePermissionDTO, InputCreatePermissionDTO } from './dto/input.dto'; +import { OutputPermissionDTO } from './dto/output.dto'; import { Permission } from './entities/permission.entity'; import { User } from '../users/entities/user.entity'; @Injectable() export class PermissionsService { - constructor(private readonly orm: MikroORM, private readonly t: TranslateService) {} + constructor(private readonly orm: MikroORM) {} /** * Automatically revoke permissions that have expired. @@ -40,14 +38,10 @@ export class PermissionsService { * @returns The created permission */ @CreateRequestContext() - async addPermissionToUser(data: PermissionPostDTO): Promise { - // Check if the permission is valid - if (!PERMISSIONS_NAMES.includes(data.permission)) - throw new BadRequestException(this.t.Errors.Permission.Invalid(data.permission)); - + async addPermissionToUser(data: InputCreatePermissionDTO): Promise { // Find the user const user = await this.orm.em.findOne(User, { id: data.id }, { populate: ['permissions'] }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, data.id)); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id: data.id }); // Check if the user already has the permission if ( @@ -56,7 +50,10 @@ export class PermissionsService { .map((p) => p.name) .includes(data.permission) ) - throw new BadRequestException(this.t.Errors.Permission.AlreadyOnUser(data.permission, user.full_name)); + throw new i18nBadRequestException('validations.permission.invalid.already_on', { + permission: data.permission, + name: user.full_name, + }); // Add the permission to the user const permission = this.orm.em.create(Permission, { @@ -68,7 +65,7 @@ export class PermissionsService { // Save it & return it await this.orm.em.persistAndFlush(permission); - return permission.toObject() as unknown as PermissionGetDTO; + return permission.toObject() as unknown as OutputPermissionDTO; } /** @@ -77,27 +74,28 @@ export class PermissionsService { * @returns {Promise} The permissions of the user */ @CreateRequestContext() - async getPermissionsOfUser(id: number): Promise { + async getPermissionsOfUser(id: number): Promise { const user = await this.orm.em.findOne(User, { id }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id }); const permissions = await user.permissions.loadItems(); - return permissions.map((p) => p.toObject() as unknown as PermissionGetDTO); + return permissions.map((p) => p.toObject() as unknown as OutputPermissionDTO); } @CreateRequestContext() - async editPermissionOfUser(data: PermissionPatchDTO): Promise { + async editPermissionOfUser(data: InputUpdatePermissionDTO): Promise { const user = await this.orm.em.findOne(User, { id: data.user_id }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, data.user_id)); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id: data.user_id }); const perm = await this.orm.em.findOne(Permission, { id: data.id, user: data.user_id }); - if (!perm) throw new NotFoundException(this.t.Errors.Permission.NotFoundOnUser(data.name, user.full_name)); + if (!perm) + throw new i18nNotFoundException('validations.permission.not_found', { id: data.id, name: user.full_name }); if (data.name) perm.name = data.name; if (data.expires) perm.expires = data.expires; if (data.revoked !== undefined) perm.revoked = data.revoked; await this.orm.em.persistAndFlush(perm); - return perm.toObject() as unknown as PermissionGetDTO; + return perm.toObject() as unknown as OutputPermissionDTO; } } diff --git a/src/modules/promotions/dto/get.dto.ts b/src/modules/promotions/dto/get.dto.ts deleted file mode 100644 index e193d84d..00000000 --- a/src/modules/promotions/dto/get.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { IPromotionPictureResponseDTO, IPromotionResponseDTO } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsInt } from 'class-validator'; - -import { BaseResponseDTO } from '@modules/_mixin/dto/base.dto'; -import { FileGetDTO } from '@modules/files/dto/get.dto'; - -export class PromotionResponseDTO extends BaseResponseDTO implements IPromotionResponseDTO { - @ApiProperty() - @IsInt() - number: number; - - @ApiProperty() - @IsInt() - users_count: number; - - @ApiProperty({ required: false }) - @IsInt() - picture?: number; -} - -export class PromotionPictureResponseDTO extends FileGetDTO implements IPromotionPictureResponseDTO { - @ApiProperty() - @IsInt() - picture_promotion_id: number; -} diff --git a/src/modules/promotions/dto/input.dto.ts b/src/modules/promotions/dto/input.dto.ts new file mode 100644 index 00000000..2a88b1f5 --- /dev/null +++ b/src/modules/promotions/dto/input.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { I18nIsId } from '@modules/_mixin/decorators'; + +export class InputPromotionNumberParamDTO { + @ApiProperty({ minimum: 1 }) + @I18nIsId() + number: number; +} diff --git a/src/modules/promotions/dto/output.dto.ts b/src/modules/promotions/dto/output.dto.ts new file mode 100644 index 00000000..449b508b --- /dev/null +++ b/src/modules/promotions/dto/output.dto.ts @@ -0,0 +1,22 @@ +import type { OutputPromotionPictureDto, OutputPromotionDto } from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; + +import { OutputBaseDTO } from '@modules/_mixin/dto/output.dto'; +import { OutputFileDTO } from '@modules/files/dto/output.dto'; + +export class OutputPromotionDTO extends OutputBaseDTO implements OutputPromotionDto { + @ApiProperty() + number: number; + + @ApiProperty() + users_count: number; + + @ApiProperty({ required: false }) + picture?: number; +} + +export class OutputPromotionPictureDTO extends OutputFileDTO implements OutputPromotionPictureDto { + @ApiProperty() + picture_promotion_id: number; +} diff --git a/src/modules/promotions/promotions.controller.ts b/src/modules/promotions/promotions.controller.ts index 388debb5..d2af4994 100644 --- a/src/modules/promotions/promotions.controller.ts +++ b/src/modules/promotions/promotions.controller.ts @@ -1,50 +1,35 @@ -import { - BadRequestException, - Controller, - Delete, - Get, - Param, - Post, - StreamableFile, - UploadedFile, - UseGuards, -} from '@nestjs/common'; +import { Controller, Delete, Get, Param, Post, Req, UploadedFile, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { z } from 'zod'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; -import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; +import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { i18nBadRequestException } from '@modules/_mixin/http-errors'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { ApiDownloadFile } from '@modules/files/decorators/download.decorator'; import { ApiUploadFile } from '@modules/files/decorators/upload.decorator'; import { FilesService } from '@modules/files/files.service'; -import { TranslateService } from '@modules/translate/translate.service'; -import { validate } from '@utils/validate'; +import { Request } from '@modules/users/entities/user.entity'; -import { PromotionResponseDTO } from './dto/get.dto'; -import { Promotion } from './entities/promotion.entity'; +import { InputPromotionNumberParamDTO } from './dto/input.dto'; +import { OutputPromotionDTO } from './dto/output.dto'; import { PromotionsService } from './promotions.service'; -import { BaseUserResponseDTO } from '../users/dto/base-user.dto'; +import { OutputBaseUserDTO } from '../users/dto/output.dto'; @ApiTags('Promotions') @Controller('promotions') @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() export class PromotionsController { - constructor( - private readonly promotionsService: PromotionsService, - private readonly filesService: FilesService, - private readonly t: TranslateService, - ) {} + constructor(private readonly promotionsService: PromotionsService, private readonly filesService: FilesService) {} @Get() @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_PROMOTION') @ApiOperation({ summary: 'Get all existing promotions' }) - @ApiOkResponse({ type: [PromotionResponseDTO] }) - async getAll(): Promise { + @ApiOkResponse({ type: [OutputPromotionDTO] }) + async getAll(): Promise { return this.promotionsService.findAll(); } @@ -52,8 +37,8 @@ export class PromotionsController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_PROMOTION') @ApiOperation({ summary: 'Get the latest promotion that has been created' }) - @ApiOkResponse({ type: PromotionResponseDTO }) - async getLatest(): Promise { + @ApiOkResponse({ type: OutputPromotionDTO }) + async getLatest(): Promise { return this.promotionsService.findLatest(); } @@ -61,8 +46,8 @@ export class PromotionsController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_PROMOTION') @ApiOperation({ summary: 'Get promotions currently active' }) - @ApiOkResponse({ type: [PromotionResponseDTO] }) - async getCurrent(): Promise { + @ApiOkResponse({ type: [OutputPromotionDTO] }) + async getCurrent(): Promise { return this.promotionsService.findCurrent(); } @@ -72,13 +57,12 @@ export class PromotionsController { @ApiOperation({ summary: 'Update the promotion logo' }) @ApiUploadFile() @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) - @ApiOkResponse({ type: PromotionResponseDTO }) + @ApiOkResponse({ type: OutputPromotionDTO }) @ApiNotOkResponses({ 400: 'Invalid file', 404: 'Promotion not found' }) - async editLogo(@UploadedFile() file: Express.Multer.File, @Param('number') number: number) { - if (!file) throw new BadRequestException(this.t.Errors.File.NotProvided()); - validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); + async editLogo(@UploadedFile() file: Express.Multer.File, @Param() params: InputPromotionNumberParamDTO) { + if (!file) throw new i18nBadRequestException('validations.file.invalid.not_provided'); - return this.promotionsService.updateLogo(number, file); + return this.promotionsService.updateLogo(params.number, file); } @Get(':number/logo') @@ -88,11 +72,9 @@ export class PromotionsController { @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiDownloadFile('The promotion logo') @ApiNotOkResponses({ 404: 'Promotion not found or promotion has no logo' }) - async getLogo(@Param('number') number: number) { - validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); - - const logo = await this.promotionsService.getLogo(number); - return new StreamableFile(this.filesService.toReadable(logo)); + async getLogo(@Req() req: Request, @Param() params: InputPromotionNumberParamDTO) { + const logo = await this.promotionsService.getLogo(params.number); + return this.filesService.getAsStreamable(logo, req.user.id); } @Delete(':number/logo') @@ -100,12 +82,10 @@ export class PromotionsController { @GuardPermissions('CAN_EDIT_PROMOTION') @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOperation({ summary: 'Delete the promotion logo' }) - @ApiOkResponse({ type: MessageResponseDTO }) + @ApiOkResponse({ type: OutputMessageDTO }) @ApiNotOkResponses({ 404: 'Promotion not found or promotion has no logo' }) - async deleteLogo(@Param('number') number: number) { - validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); - - return this.promotionsService.deleteLogo(number); + async deleteLogo(@Param() params: InputPromotionNumberParamDTO) { + return this.promotionsService.deleteLogo(params.number); } @Get(':number') @@ -113,12 +93,10 @@ export class PromotionsController { @GuardPermissions('CAN_READ_PROMOTION') @ApiOperation({ summary: 'Get the specified promotion' }) @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) - @ApiOkResponse({ type: PromotionResponseDTO }) + @ApiOkResponse({ type: OutputPromotionDTO }) @ApiNotOkResponses({ 404: 'Promotion not found' }) - async get(@Param('number') number: number) { - validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); - - return this.promotionsService.findOne(number); + async get(@Param() params: InputPromotionNumberParamDTO) { + return this.promotionsService.findOne(params.number); } @Get(':number/users') @@ -126,11 +104,9 @@ export class PromotionsController { @GuardPermissions('CAN_READ_PROMOTION') @ApiParam({ name: 'number', description: 'The promotion number (eg: 21)' }) @ApiOperation({ summary: 'Get users of the specified promotions' }) - @ApiOkResponse({ type: [BaseUserResponseDTO] }) + @ApiOkResponse({ type: [OutputBaseUserDTO] }) @ApiNotOkResponses({ 404: 'Promotion not found' }) - async getUsers(@Param('number') number: number) { - validate(z.coerce.number().int().min(1), number, this.t.Errors.Id.Invalid(Promotion, number)); - - return this.promotionsService.getUsers(number); + async getUsers(@Param() params: InputPromotionNumberParamDTO) { + return this.promotionsService.getUsers(params.number); } } diff --git a/src/modules/promotions/promotions.module.ts b/src/modules/promotions/promotions.module.ts index 84fd4129..67c46064 100644 --- a/src/modules/promotions/promotions.module.ts +++ b/src/modules/promotions/promotions.module.ts @@ -6,7 +6,6 @@ import { AuthService } from '@modules/auth/auth.service'; import { EmailsService } from '@modules/emails/emails.service'; import { FilesService } from '@modules/files/files.service'; import { ImagesService } from '@modules/files/images.service'; -import { TranslateService } from '@modules/translate/translate.service'; import { UsersDataService } from '@modules/users/services/users-data.service'; import { PromotionPicture } from './entities/promotion-picture.entity'; @@ -16,16 +15,7 @@ import { PromotionsService } from './promotions.service'; @Module({ imports: [MikroOrmModule.forFeature([Promotion, PromotionPicture])], - providers: [ - PromotionsService, - JwtService, - UsersDataService, - FilesService, - EmailsService, - AuthService, - TranslateService, - ImagesService, - ], + providers: [PromotionsService, JwtService, UsersDataService, FilesService, EmailsService, AuthService, ImagesService], controllers: [PromotionsController], exports: [PromotionsService], }) diff --git a/src/modules/promotions/promotions.service.ts b/src/modules/promotions/promotions.service.ts index 71a312c1..bc2172d0 100644 --- a/src/modules/promotions/promotions.service.ts +++ b/src/modules/promotions/promotions.service.ts @@ -1,26 +1,22 @@ import { join } from 'path'; import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { env } from '@env'; -import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; -import { FilesService } from '@modules/files/files.service'; -import { TranslateService } from '@modules/translate/translate.service'; +import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { ImagesService } from '@modules/files/images.service'; -import { PromotionPictureResponseDTO, PromotionResponseDTO } from './dto/get.dto'; +import { OutputPromotionPictureDTO, OutputPromotionDTO } from './dto/output.dto'; import { PromotionPicture } from './entities/promotion-picture.entity'; import { Promotion } from './entities/promotion.entity'; -import { BaseUserResponseDTO } from '../users/dto/base-user.dto'; +import { OutputBaseUserDTO } from '../users/dto/output.dto'; @Injectable() export class PromotionsService { - constructor( - private readonly t: TranslateService, - private readonly orm: MikroORM, - private readonly filesService: FilesService, - ) {} + constructor(private readonly orm: MikroORM, private readonly imagesService: ImagesService) {} /** * Create a new promotion each year on the 15th of July @@ -35,14 +31,14 @@ export class PromotionsService { } @CreateRequestContext() - async findAll(): Promise { + async findAll(): Promise { return (await this.orm.em.find(Promotion, {}, { fields: ['*', 'users'] })).map( - (p) => p.toObject() as unknown as PromotionResponseDTO, + (p) => p.toObject() as unknown as OutputPromotionDTO, ); } @CreateRequestContext() - async findLatest(): Promise { + async findLatest(): Promise { const promotion = ( await this.orm.em.find( Promotion, @@ -50,34 +46,34 @@ export class PromotionsService { { orderBy: { number: 'DESC' }, limit: 1, fields: ['*', 'picture', 'users'] }, ) )[0]; - return promotion.toObject() as unknown as PromotionResponseDTO; + return promotion.toObject() as unknown as OutputPromotionDTO; } @CreateRequestContext() - async findCurrent(): Promise { + async findCurrent(): Promise { return ( await this.orm.em.find( Promotion, {}, { orderBy: { number: 'DESC' }, limit: 5, fields: ['*', 'picture', 'users'] }, ) - ).map((p) => p.toObject() as unknown as PromotionResponseDTO); + ).map((p) => p.toObject() as unknown as OutputPromotionDTO); } @CreateRequestContext() - async findOne(number: number): Promise { + async findOne(number: number): Promise { const promotion = await this.orm.em.findOne(Promotion, { number }, { fields: ['*', 'picture', 'users'] }); - if (!promotion) throw new NotFoundException(this.t.Errors.Id.NotFound(Promotion, number)); + if (!promotion) throw new i18nNotFoundException('validations.promotion.invalid.not_found', { number }); - return promotion.toObject() as unknown as PromotionResponseDTO; + return promotion.toObject() as unknown as OutputPromotionDTO; } @CreateRequestContext() - async getUsers(number: number): Promise { + async getUsers(number: number): Promise { const promotion = await this.orm.em.findOne(Promotion, { number }, { fields: ['users'] }); - if (!promotion) throw new NotFoundException(this.t.Errors.Id.NotFound(Promotion, number)); + if (!promotion) throw new i18nNotFoundException('validations.promotion.invalid.not_found', { number }); - const res: BaseUserResponseDTO[] = []; + const res: OutputBaseUserDTO[] = []; for (const user of promotion.users.getItems()) { res.push({ @@ -94,19 +90,19 @@ export class PromotionsService { } @CreateRequestContext() - async updateLogo(number: number, file: Express.Multer.File): Promise { + async updateLogo(number: number, file: Express.Multer.File): Promise { const promotion = await this.orm.em.findOne(Promotion, { number }, { populate: ['picture'] }); - if (!promotion) throw new NotFoundException(this.t.Errors.Id.NotFound(Promotion, number)); + if (!promotion) throw new i18nNotFoundException('validations.promotion.invalid.not_found', { number }); - const fileInfos = await this.filesService.writeOnDiskAsImage(file, { + const fileInfos = await this.imagesService.writeOnDisk(file.buffer, { directory: join(env.PROMOTION_BASE_PATH, 'logo'), filename: `promotion_${promotion.number}`, aspect_ratio: '1:1', }); if (promotion.picture) { - this.filesService.deleteFromDisk(promotion.picture); + this.imagesService.deleteFromDisk(promotion.picture); promotion.picture.filename = fileInfos.filename; promotion.picture.mimetype = fileInfos.mimetype; @@ -124,30 +120,30 @@ export class PromotionsService { }); await this.orm.em.persistAndFlush(promotion); - return promotion.picture.toObject() as unknown as PromotionPictureResponseDTO; + return promotion.picture.toObject() as unknown as OutputPromotionPictureDTO; } @CreateRequestContext() async getLogo(number: number): Promise { const promotion = await this.orm.em.findOne(Promotion, { number }, { populate: ['picture'] }); - if (!promotion) throw new NotFoundException(this.t.Errors.Id.NotFound(Promotion, number)); + if (!promotion) throw new i18nNotFoundException('validations.promotion.invalid.not_found', { number }); - if (!promotion.picture) throw new NotFoundException(this.t.Errors.Promotion.LogoNotFound(number)); + if (!promotion.picture) throw new i18nNotFoundException('validations.promotion.invalid.no_logo', { number }); delete promotion.picture.picture_promotion; // avoid circular reference return promotion.picture; } @CreateRequestContext() - async deleteLogo(number: number): Promise { + async deleteLogo(number: number): Promise { const promotion = await this.orm.em.findOne(Promotion, { number }, { populate: ['picture'] }); - if (!promotion) throw new NotFoundException(this.t.Errors.Id.NotFound(Promotion, number)); + if (!promotion) throw new i18nNotFoundException('validations.promotion.invalid.not_found', { number }); - if (!promotion.picture) throw new NotFoundException(this.t.Errors.Promotion.LogoNotFound(number)); + if (!promotion.picture) throw new i18nNotFoundException('validations.promotion.invalid.no_logo', { number }); - this.filesService.deleteFromDisk(promotion.picture); + this.imagesService.deleteFromDisk(promotion.picture); await this.orm.em.removeAndFlush(promotion.picture); - return { message: this.t.Success.Entity.Deleted(PromotionPicture), statusCode: 200 }; + return new OutputMessageDTO('validations.promotion.success.deleted_logo', { number: promotion.number }); } } diff --git a/src/modules/roles/dto/get.dto.ts b/src/modules/roles/dto/get.dto.ts deleted file mode 100644 index 70d2b970..00000000 --- a/src/modules/roles/dto/get.dto.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { IRoleGetDTO, IRoleUsersResponseDTO, PERMISSION_NAMES } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsBoolean, IsDate, IsInt, IsString } from 'class-validator'; - -import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -import { BaseUserResponseDTO } from '@modules/users/dto/base-user.dto'; - -export class RoleUsersResponseDTO extends BaseUserResponseDTO implements IRoleUsersResponseDTO { - @ApiProperty({ type: Date }) - @IsDate() - role_expires: Date; -} - -export class RoleGetDTO implements IRoleGetDTO { - @ApiProperty({ type: String, example: 'AE_ADMIN' }) - @IsString() // TODO: Add custom validator to check if it's uppercase - name: Uppercase; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - revoked: boolean; - - @ApiProperty({ enum: PERMISSIONS_NAMES, isArray: true }) - @IsArray() // TODO: Add custom validator to check if it's array of uppercase strings - permissions: PERMISSION_NAMES[]; - - @ApiProperty({ type: Number, default: 1 }) - @IsInt() - users_count: number; -} diff --git a/src/modules/roles/dto/input.dto.ts b/src/modules/roles/dto/input.dto.ts new file mode 100644 index 00000000..ae16db27 --- /dev/null +++ b/src/modules/roles/dto/input.dto.ts @@ -0,0 +1,72 @@ +import type { + InputUpdateRoleDto, + InputUpdateRoleUserDto, + InputCreateRoleDto, + PERMISSION_NAMES, + I18nTranslations, + InputUpdateRoleUsersDto, + InputDeleteRoleUsersDto, +} from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { ArrayNotEmpty, ArrayUnique, IsEnum, IsUppercase, ValidateNested } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; +import { I18nIsDate, I18nIsId, I18nIsString } from '@modules/_mixin/decorators'; + +export class InputCreateRoleDTO implements InputCreateRoleDto { + @ApiProperty({ type: String, example: 'AE_ADMINS' }) + @I18nIsString() + @IsUppercase({ message: i18nValidationMessage('validations.string.invalid.uppercase') }) + name: Uppercase; + + @ApiProperty({ enum: PERMISSIONS_NAMES, isArray: true }) + @IsEnum(PERMISSIONS_NAMES, { + each: true, + message: i18nValidationMessage('validations.permission.invalid.format', { + permissions: PERMISSIONS_NAMES.join("', '"), + }), + }) + @ArrayUnique({ message: i18nValidationMessage('validations.array.invalid.duplicate') }) + @ArrayNotEmpty({ message: i18nValidationMessage('validations.array.invalid.not_empty') }) + permissions: PERMISSION_NAMES[]; +} + +export class InputUpdateRoleDTO extends InputCreateRoleDTO implements InputUpdateRoleDto { + @ApiProperty({ required: true, minimum: 1 }) + @I18nIsId() + id: number; +} + +export class InputUpdateRoleUserDTO implements InputUpdateRoleUserDto { + @ApiProperty({ required: true, minimum: 1 }) + @I18nIsId() + id: number; + + @ApiProperty({ required: true }) + @I18nIsDate() + expires: Date; +} + +export class InputUpdateRoleUsersDTO implements InputUpdateRoleUsersDto { + @ApiProperty({ type: InputUpdateRoleUserDTO, isArray: true }) + @ArrayNotEmpty({ message: i18nValidationMessage('validations.array.invalid.not_empty') }) + @ArrayUnique({ message: i18nValidationMessage('validations.array.invalid.duplicate') }) + @ValidateNested({ + each: true, + message: i18nValidationMessage('validations.array.invalid.format', { + type: InputUpdateRoleUserDTO.name, + }), + }) + @Type(() => InputUpdateRoleUserDTO) + users: InputUpdateRoleUserDTO[]; +} + +export class InputDeleteRoleUsersDTO implements InputDeleteRoleUsersDto { + @ApiProperty({ isArray: true, type: Number }) + @ArrayNotEmpty({ message: i18nValidationMessage('validations.array.invalid.not_empty') }) + @I18nIsId({ each: true }) + users: number[]; +} diff --git a/src/modules/roles/dto/output.dto.ts b/src/modules/roles/dto/output.dto.ts new file mode 100644 index 00000000..e1a4a8d6 --- /dev/null +++ b/src/modules/roles/dto/output.dto.ts @@ -0,0 +1,25 @@ +import type { OutputRoleDto, OutputRoleUserDto, PERMISSION_NAMES } from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; + +import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; +import { OutputBaseUserDTO } from '@modules/users/dto/output.dto'; + +export class OutputRoleUserDTO extends OutputBaseUserDTO implements OutputRoleUserDto { + @ApiProperty({ type: Date }) + role_expires: Date; +} + +export class OutputRoleDTO implements OutputRoleDto { + @ApiProperty({ type: String, example: 'AE_ADMIN' }) + name: Uppercase; + + @ApiProperty({ type: Boolean, default: false }) + revoked: boolean; + + @ApiProperty({ enum: PERMISSIONS_NAMES, isArray: true }) + permissions: PERMISSION_NAMES[]; + + @ApiProperty({ type: Number, default: 1 }) + users_count: number; +} diff --git a/src/modules/roles/dto/patch.dto.ts b/src/modules/roles/dto/patch.dto.ts deleted file mode 100644 index c16ec503..00000000 --- a/src/modules/roles/dto/patch.dto.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { IRolePatchDTO, IRoleEditUserDTO } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsDate, IsInt } from 'class-validator'; - -import { RolePostDTO } from './post.dto'; - -export class RolePatchDTO extends RolePostDTO implements IRolePatchDTO { - @ApiProperty({ required: true, minimum: 1 }) - @IsInt() - id: number; -} - -export class RoleEditUserDTO implements IRoleEditUserDTO { - @ApiProperty({ required: true, type: Number, minimum: 1 }) - @IsInt() - id: number; - - @ApiProperty({ required: true, type: Date }) - @IsDate() - expires: Date; -} diff --git a/src/modules/roles/dto/post.dto.ts b/src/modules/roles/dto/post.dto.ts deleted file mode 100644 index 66dd460c..00000000 --- a/src/modules/roles/dto/post.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { IRolePostDTO, PERMISSION_NAMES } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; - -export class RolePostDTO implements IRolePostDTO { - @ApiProperty({ type: String, example: 'AE_ADMINS' }) - @IsString() - name: Uppercase; - - @ApiProperty({ enum: PERMISSIONS_NAMES, isArray: true }) - @IsString() - permissions: PERMISSION_NAMES[]; -} diff --git a/src/modules/roles/roles.controller.ts b/src/modules/roles/roles.controller.ts index 489cb413..e852b85e 100644 --- a/src/modules/roles/roles.controller.ts +++ b/src/modules/roles/roles.controller.ts @@ -1,18 +1,19 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiTags, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiBody, ApiParam } from '@nestjs/swagger'; -import { z } from 'zod'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/_mixin/dto/input.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; -import { TranslateService } from '@modules/translate/translate.service'; -import { validate } from '@utils/validate'; -import { RoleGetDTO, RoleUsersResponseDTO } from './dto/get.dto'; -import { RoleEditUserDTO, RolePatchDTO } from './dto/patch.dto'; -import { RolePostDTO } from './dto/post.dto'; -import { Role } from './entities/role.entity'; +import { + InputUpdateRoleDTO, + InputCreateRoleDTO, + InputUpdateRoleUsersDTO, + InputDeleteRoleUsersDTO, +} from './dto/input.dto'; +import { OutputRoleDTO, OutputRoleUserDTO } from './dto/output.dto'; import { RolesService } from './roles.service'; @ApiTags('Roles') @@ -20,23 +21,15 @@ import { RolesService } from './roles.service'; @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() export class RolesController { - constructor(private readonly rolesService: RolesService, private readonly t: TranslateService) {} + constructor(private readonly rolesService: RolesService) {} @Post() @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_ROLE') @ApiOperation({ summary: 'Create a new role' }) - @ApiOkResponse({ type: RoleGetDTO }) + @ApiOkResponse({ type: OutputRoleDTO }) @ApiNotOkResponses({ 400: 'Role name is not uppercase or already exists' }) - async createRole(@Body() body: RolePostDTO) { - const schema = z - .object({ - name: z.string().refine((name) => name === name.toUpperCase(), {}), - permissions: z.array(z.string()), - }) - .strict(); - validate(schema, body); - + async createRole(@Body() body: InputCreateRoleDTO) { return this.rolesService.createRole(body.name, body.permissions); } @@ -44,21 +37,9 @@ export class RolesController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_ROLE') @ApiOperation({ summary: 'Update an existing role' }) - @ApiOkResponse({ type: RoleGetDTO }) + @ApiOkResponse({ type: OutputRoleDTO }) @ApiNotOkResponses({ 400: 'Role name is not uppercase', 404: 'Role not found' }) - async editRole(@Body() body: RolePatchDTO) { - const schema = z - .object({ - id: z.number(), - name: z - .string() - .refine((name) => name === name.toUpperCase(), {}) - .optional(), - permissions: z.array(z.string()).optional(), - }) - .strict(); - validate(schema, body); - + async editRole(@Body() body: InputUpdateRoleDTO) { return this.rolesService.editRole(body); } @@ -66,73 +47,54 @@ export class RolesController { @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_ROLE') @ApiOperation({ summary: 'Get all existing roles' }) - @ApiOkResponse({ type: [RoleGetDTO] }) + @ApiOkResponse({ type: [OutputRoleDTO] }) async getAllRoles() { return this.rolesService.getAllRoles(); } - @Get(':role_id') + @Get(':id') @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_ROLE') @ApiOperation({ summary: 'Get the specified role' }) - @ApiParam({ name: 'role_id', description: 'The role ID' }) - @ApiOkResponse({ type: RoleGetDTO }) + @ApiParam({ name: 'id', description: 'The role ID' }) + @ApiOkResponse({ type: OutputRoleDTO }) @ApiNotOkResponses({ 400: 'Invalid role ID', 404: 'Role not found' }) - async getRole(@Param('role_id') id: number) { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(Role, id)); - - return this.rolesService.getRole(id); + async getRole(@Param() params: InputIdParamDTO) { + return this.rolesService.getRole(params.id); } - @Get(':role_id/users') + @Get(':id/users') @UseGuards(PermissionGuard) @GuardPermissions('CAN_READ_ROLE') @ApiOperation({ summary: 'Get user(s) of the specified role' }) @ApiParam({ name: 'role_id', description: 'The role ID' }) - @ApiOkResponse({ type: [RoleUsersResponseDTO] }) + @ApiOkResponse({ type: [OutputRoleUserDTO] }) @ApiNotOkResponses({ 400: 'Invalid role ID', 404: 'Role not found' }) - async getRoleUsers(@Param('role_id') id: number) { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(Role, id)); - - return this.rolesService.getUsers(id); + async getRoleUsers(@Param() params: InputIdParamDTO) { + return this.rolesService.getUsers(params.id); } - @Post(':role_id/users') + @Post(':id/users') @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_ROLE') @ApiOperation({ summary: 'Add user(s) to the role' }) - @ApiParam({ name: 'role_id', description: 'The role ID' }) - @ApiOkResponse({ type: [RoleUsersResponseDTO] }) + @ApiParam({ name: 'id', description: 'The role ID' }) + @ApiOkResponse({ type: [OutputRoleUserDTO] }) @ApiNotOkResponses({ 400: 'Invalid role ID or body', 404: 'Role not found' }) - @ApiBody({ type: [RoleEditUserDTO] }) - async addUsersToRole(@Param('role_id') role_id: number, @Body() body: RoleEditUserDTO[]) { - validate(z.coerce.number().int().min(1), role_id, this.t.Errors.Id.Invalid(Role, role_id)); - - const schema = z - .object({ - id: z.number().int().min(1).positive(), - expires: z.string().datetime(), - }) - .strict(); - validate(z.array(schema).min(1), body); - - return this.rolesService.addUsers(role_id, body); + @ApiBody({ type: [InputUpdateRoleUsersDTO] }) + async addUsersToRole(@Param() params: InputIdParamDTO, @Body() body: InputUpdateRoleUsersDTO) { + return this.rolesService.addUsers(params.id, body.users); } - @Delete(':role_id/users') + @Delete(':id/users') @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_ROLE') @ApiOperation({ summary: 'Remove user(s) from the role' }) - @ApiParam({ name: 'role_id', description: 'The role ID' }) - @ApiOkResponse({ type: [RoleUsersResponseDTO] }) + @ApiParam({ name: 'id', description: 'The role ID' }) + @ApiOkResponse({ type: [OutputRoleUserDTO] }) @ApiNotOkResponses({ 400: 'Invalid role ID or given users IDs', 404: 'Role not found' }) @ApiBody({ type: [Number] }) - async removeUsersToRole(@Param('role_id') role_id: number, @Body() body: number[]) { - validate(z.coerce.number().int().min(1), role_id, this.t.Errors.Id.Invalid(Role, role_id)); - - const schema = z.array(z.number().min(1)).min(1); - validate(schema, body); - - return this.rolesService.removeUsers(role_id, body); + async removeUsersToRole(@Param() params: InputIdParamDTO, @Body() body: InputDeleteRoleUsersDTO) { + return this.rolesService.removeUsers(params.id, body.users); } } diff --git a/src/modules/roles/roles.module.ts b/src/modules/roles/roles.module.ts index 45878e50..e20ca528 100644 --- a/src/modules/roles/roles.module.ts +++ b/src/modules/roles/roles.module.ts @@ -3,7 +3,6 @@ import { Module } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { AuthService } from '@modules/auth/auth.service'; -import { TranslateService } from '@modules/translate/translate.service'; import { UsersModule } from '@modules/users/users.module'; import { Role } from './entities/role.entity'; @@ -12,7 +11,7 @@ import { RolesService } from './roles.service'; @Module({ imports: [MikroOrmModule.forFeature([Role]), UsersModule], - providers: [RolesService, JwtService, AuthService, TranslateService], + providers: [RolesService, JwtService, AuthService], controllers: [RolesController], exports: [RolesService], }) diff --git a/src/modules/roles/roles.service.ts b/src/modules/roles/roles.service.ts index 2b4ca751..7eea54e5 100644 --- a/src/modules/roles/roles.service.ts +++ b/src/modules/roles/roles.service.ts @@ -1,27 +1,22 @@ import type { PERMISSION_NAMES } from '#types/api'; import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -import { TranslateService } from '@modules/translate/translate.service'; -import { BaseUserResponseDTO } from '@modules/users/dto/base-user.dto'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { OutputBaseUserDTO } from '@modules/users/dto/output.dto'; import { User } from '@modules/users/entities/user.entity'; import { UsersDataService } from '@modules/users/services/users-data.service'; -import { RoleGetDTO, RoleUsersResponseDTO } from './dto/get.dto'; -import { RolePatchDTO } from './dto/patch.dto'; +import { InputUpdateRoleDTO } from './dto/input.dto'; +import { OutputRoleDTO, OutputRoleUserDTO } from './dto/output.dto'; import { RoleExpiration } from './entities/role-expiration.entity'; import { Role } from './entities/role.entity'; @Injectable() export class RolesService { - constructor( - private readonly orm: MikroORM, - private readonly t: TranslateService, - private readonly usersService: UsersDataService, - ) {} + constructor(private readonly orm: MikroORM, private readonly usersService: UsersDataService) {} /** * Automatically revoke roles that have expired @@ -47,55 +42,48 @@ export class RolesService { * @returns the array of all roles */ @CreateRequestContext() - async getAllRoles(): Promise { + async getAllRoles(): Promise { const roles = await this.orm.em.find(Role, {}, { populate: ['users'] }); - return roles.map((r) => r.toObject() as unknown as RoleGetDTO); + return roles.map((r) => r.toObject() as unknown as OutputRoleDTO); } @CreateRequestContext() - async getRole(id: number): Promise { + async getRole(id: number): Promise { const role = await this.orm.em.findOne(Role, { id }, { populate: ['users'] }); - if (!role) throw new NotFoundException(this.t.Errors.Id.NotFound(Role, id)); + if (!role) throw new i18nNotFoundException('validations.role.not_found', { id }); - return role.toObject() as unknown as RoleGetDTO; + return role.toObject() as unknown as OutputRoleDTO; } @CreateRequestContext() - async createRole(name: string, permissions: PERMISSION_NAMES[]): Promise { + async createRole(name: string, permissions: PERMISSION_NAMES[]): Promise { const roleName = name.toUpperCase(); if (await this.orm.em.findOne(Role, { name: roleName })) - throw new BadRequestException(this.t.Errors.Role.NameAlreadyUsed(roleName)); + throw new i18nBadRequestException('validations.role.invalid.already_exist', { name: roleName }); - // Remove eventual duplicates - permissions = permissions.unique(); - - permissions.forEach((p) => { - if (!PERMISSIONS_NAMES.includes(p)) throw new BadRequestException(this.t.Errors.Permission.Invalid(p)); - }); - - const role = this.orm.em.create(Role, { name: roleName, permissions }); + const role = this.orm.em.create(Role, { name: roleName, permissions: permissions.unique() }); await this.orm.em.persistAndFlush(role); - return role.toObject() as unknown as RoleGetDTO; + return role.toObject() as unknown as OutputRoleDTO; } @CreateRequestContext() - async editRole(input: RolePatchDTO): Promise { + async editRole(input: InputUpdateRoleDTO): Promise { const role = await this.orm.em.findOne(Role, { id: input.id }, { populate: ['users'] }); - if (!role) throw new NotFoundException(this.t.Errors.Id.NotFound(Role, input.id)); + if (!role) throw new i18nNotFoundException('validations.role.not_found', { id: input.id }); role.name = input.name.toUpperCase(); role.permissions = input.permissions.unique(); await this.orm.em.persistAndFlush(role); - return role.toObject() as unknown as RoleGetDTO; + return role.toObject() as unknown as OutputRoleDTO; } @CreateRequestContext() - async getUsers(id: number): Promise> { + async getUsers(id: number): Promise> { const role = await this.orm.em.findOne(Role, { id }, { populate: ['users'] }); - if (!role) throw new NotFoundException(this.t.Errors.Id.NotFound(Role, id)); + if (!role) throw new i18nNotFoundException('validations.role.not_found', { id }); const role_expirations = await this.orm.em.find(RoleExpiration, { role: { id }, revoked: false }); const users_ids = role_expirations.map((r) => r.user.id).unique(); @@ -107,14 +95,14 @@ export class RolesService { @CreateRequestContext() async addUsers(role_id: number, users_specs: Array<{ id: number; expires: Date }>) { const role = await this.orm.em.findOne(Role, { id: role_id }, { populate: ['users'] }); - if (!role) throw new NotFoundException(this.t.Errors.Id.NotFound(Role, role_id)); + if (!role) throw new i18nNotFoundException('validations.role.not_found', { id: role_id }); const users_ids = users_specs.map((u) => u.id); const users = await this.orm.em.find(User, { id: { $in: users_ids } }); // If one user is not found, throw an error if (users.length !== users_ids.length) - throw new NotFoundException(this.t.Errors.Id.NotFound(User, users_ids.join(', '))); + throw new i18nNotFoundException('validations.users.not_found.ids', { ids: users_ids.join("', '") }); role.users.add(users); @@ -130,12 +118,13 @@ export class RolesService { } @CreateRequestContext() - async removeUsers(role_id: number, users_id: number[]): Promise { + async removeUsers(role_id: number, users_id: number[]): Promise { const role = await this.orm.em.findOne(Role, { id: role_id }, { populate: ['users'] }); - if (!role) throw new NotFoundException(this.t.Errors.Id.NotFound(Role, role_id)); + if (!role) throw new i18nNotFoundException('validations.role.not_found', { id: role_id }); const users = await this.orm.em.find(User, { id: { $in: users_id } }); - if (!users.length) throw new NotFoundException(this.t.Errors.Id.NotFound(User, users_id.join(', '))); + if (!users.length) + throw new i18nNotFoundException('validations.users.not_found.ids', { ids: users_id.join("', '") }); const roleExpirations = await this.orm.em.find(RoleExpiration, { role, diff --git a/src/modules/translate/translate.module.ts b/src/modules/translate/translate.module.ts deleted file mode 100644 index 5129f777..00000000 --- a/src/modules/translate/translate.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { TranslateService } from './translate.service'; - -@Module({ - imports: [], - providers: [TranslateService], - exports: [TranslateService], -}) -export class TranslateModule {} diff --git a/src/modules/translate/translate.service.ts b/src/modules/translate/translate.service.ts deleted file mode 100644 index 01405737..00000000 --- a/src/modules/translate/translate.service.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* istanbul ignore file */ - -import type { aspect_ratio, email } from '#types'; -import type { I18nTranslations, PERMISSION_NAMES } from '#types/api'; - -import { Injectable } from '@nestjs/common'; -import { PathImpl2 } from '@nestjs/config'; -import { I18nContext, I18nService } from 'nestjs-i18n'; -import { Class } from 'type-fest'; - -@Injectable() -export class TranslateService { - constructor(private readonly i18n: I18nService) {} - - private generic(key: PathImpl2, args: Record): string { - return this.i18n.t(key, { - lang: I18nContext.current()?.lang ?? undefined, // undefined -> fallback language - args: { - ...args, - - // Either we have a class -> get its name, or we have a string -> use it directly - // > Undefined -> fallback to undefined - type: args['type'] ? (typeof args['type'] === 'function' ? args['type'].name : args['type']) : undefined, - }, - }); - } - - public readonly Errors = { - BirthDate: { - Invalid: (date: Date | string) => - this.generic('responses.errors.birth_date.invalid', { - date: typeof date === 'string' ? date : date.toISOString(), - }), - }, - Email: { - AreAlreadyUsed: (emails: email[]) => - this.generic('responses.errors.email.are_used', { emails: emails.sort().join(', ') }), - IsAlreadyUsed: (email: email) => this.generic('responses.errors.email.used', { email }), - AlreadyVerified: (type: Class | string) => - this.generic('responses.errors.email.already_verified', { type }), - Blacklisted: (email: email) => this.generic('responses.errors.email.blacklisted', { email }), - Invalid: (email: email) => this.generic('responses.errors.email.invalid', { email }), - InvalidVerificationToken: () => this.generic('responses.errors.email.token.invalid', {}), - Malformed: (email: email) => this.generic('responses.errors.email.malformed', { email }), - NotVerified: (type: Class | string) => this.generic('responses.errors.email.unverified', { type }), - NotFound: (type: Class | string, email: email) => - this.generic('responses.errors.email.not_found', { type, email }), - }, - Entity: { - NotFound: (type: Class | string, field: string, value: string) => - this.generic('responses.errors.entity.not_found', { type, field, value: value }), - }, - File: { - Infected: (file: string) => this.generic('responses.errors.file.infected', { file }), - InvalidMimeType: (mime_type: string[]) => - this.generic('responses.errors.file.invalid_mime_type', { mime_type: mime_type.join(', ') }), - NotProvided: () => this.generic('responses.errors.file.no_file', {}), - NotFoundOnDisk: (file: string) => this.generic('responses.errors.file.not_found_on_disk', { file }), - UndefinedMimeType: () => this.generic('responses.errors.file.undefined_mime_type', {}), - Unauthorized: (visibility_group: string) => - this.generic('responses.errors.file.unauthorized', { visibility_group }), - }, - Id: { - Invalid: (type: Class | string, id: string | number) => - this.generic('responses.errors.id.invalid', { type, id }), - Invalids: (type: Class | string, ids: (string | number)[]) => - this.generic('responses.errors.id.invalids', { type, ids: ids.join(', ') }), - NotFound: (type: Class | string, id: string | number) => - this.generic('responses.errors.id.not_found', { type, id }), - NotFounds: (type: Class | string, ids: (string | number)[]) => - this.generic('responses.errors.id.not_founds', { type, ids: ids.join(', ') }), - }, - Image: { - InvalidAspectRatio: (aspect_ratio: aspect_ratio) => - this.generic('responses.errors.image.invalid_aspect_ratio', { aspect_ratio }), - }, - JWT: { - Expired: () => this.generic('responses.errors.jwt.expired', {}), - Invalid: () => this.generic('responses.errors.jwt.invalid', {}), - Unknown: () => this.generic('responses.errors.jwt.unknown', {}), - }, - Password: { - Mismatch: () => this.generic('responses.errors.password.mismatch', {}), - Weak: () => this.generic('responses.errors.password.weak', {}), - }, - Permission: { - AlreadyOnUser: (permission: PERMISSION_NAMES, user: string) => - this.generic('responses.errors.permission.already_on_user', { permission, user }), - Invalid: (permission: string) => this.generic('responses.errors.permission.invalid', { permission }), - NotFoundOnUser: (permission: PERMISSION_NAMES, user: string) => - this.generic('responses.errors.permission.not_found_on_user', { permission, user }), - }, - Promotion: { - LogoNotFound: (number: number) => this.generic('responses.errors.promotion.logo_not_found', { number }), - }, - Role: { - NameAlreadyUsed: (name: string) => this.generic('responses.errors.role.name_used', { name }), - }, - User: { - CannotUpdateBirthDateOrName: () => this.generic('responses.errors.user.cannot_update_birth_date_or_name', {}), - PictureCooldown: (time_left: number) => this.generic('responses.errors.user.picture_cooldown', { time_left }), - NoPicture: (user_id: number | string) => this.generic('responses.errors.user.no_picture', { user_id }), - NoBanner: (user_id: number | string) => this.generic('responses.errors.user.no_banner', { user_id }), - }, - }; - - public readonly Success = { - Entity: { - Deleted: (type: Class | string) => this.generic('responses.success.deleted', { type }), - }, - Email: { - Verified: (email: email) => this.generic('responses.success.email.verified', { email }), - }, - User: { - Registered: () => this.generic('responses.success.user.register', {}), - }, - }; -} diff --git a/src/modules/users/controllers/users-data.controller.ts b/src/modules/users/controllers/users-data.controller.ts index e4a5616f..d3b833db 100644 --- a/src/modules/users/controllers/users-data.controller.ts +++ b/src/modules/users/controllers/users-data.controller.ts @@ -1,28 +1,22 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Req, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiBody, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { z } from 'zod'; -import { USER_GENDER } from '@exported/api/constants/genders'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; -import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/_mixin/dto/input.dto'; +import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; -import { GuardSelfOrPermsOrSub } from '@modules/auth/decorators/self-or-sub-perms.decorator'; import { GuardSelfParam } from '@modules/auth/decorators/self.decorator'; -import { CreateUserDTO } from '@modules/auth/dto/post.dto'; +import { InputRegisterUsersAdminDTO } from '@modules/auth/dto/input.dto'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { SelfOrPermissionGuard } from '@modules/auth/guards/self-or-perms.guard'; -import { SelfOrPermsOrSubGuard } from '@modules/auth/guards/self-or-sub-or-perms.guard'; import { SelfGuard } from '@modules/auth/guards/self.guard'; -import { PermissionGetDTO } from '@modules/permissions/dto/get.dto'; -import { TranslateService } from '@modules/translate/translate.service'; -import { validate } from '@utils/validate'; +import { OutputPermissionDTO } from '@modules/permissions/dto/output.dto'; -import { BaseUserResponseDTO } from '../dto/base-user.dto'; -import { UserGetDTO, UserRoleGetDTO, UserVisibilityGetDTO } from '../dto/get.dto'; -import { UserPatchDTO, UserVisibilityPatchDTO } from '../dto/patch.dto'; -import { Request, User } from '../entities/user.entity'; +import { InputUpdateUserDTO, InputUpdateUserVisibilityDTO } from '../dto/input.dto'; +import { OutputUserDTO, OutputBaseUserDTO, OutputUserRoleDTO, OutputUserVisibilityDTO } from '../dto/output.dto'; +import { Request } from '../entities/user.entity'; import { UsersDataService } from '../services/users-data.service'; @ApiTags('Users') @@ -30,60 +24,32 @@ import { UsersDataService } from '../services/users-data.service'; @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() export class UsersDataController { - constructor(private readonly usersService: UsersDataService, private readonly t: TranslateService) {} + constructor(private readonly usersService: UsersDataService) {} @Post() @UseGuards(PermissionGuard) @GuardPermissions('CAN_EDIT_USER') @ApiOperation({ summary: 'Creates new users' }) - @ApiOkResponse({ description: 'The created user', type: [BaseUserResponseDTO] }) + @ApiOkResponse({ description: 'The created user', type: [OutputBaseUserDTO] }) @ApiNotOkResponses({ 400: 'Invalid input', 401: 'Insufficient permission' }) - @ApiBody({ type: [CreateUserDTO] }) - async create(@Body() input: CreateUserDTO[]): Promise { - const schema = z - .object({ - email: z.string().email(), - birth_date: z.string().datetime(), - first_name: z.string(), - last_name: z.string(), - }) - .strict(); - - validate(z.array(schema).min(1), input); - - return this.usersService.registerByAdmin(input); + @ApiBody({ type: [InputRegisterUsersAdminDTO] }) + async create(@Body() input: InputRegisterUsersAdminDTO): Promise { + return this.usersService.registerByAdmin(input.users); } - @Patch() + @Patch(':id') @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) - @ApiOperation({ summary: 'Update users data' }) - @ApiOkResponse({ description: 'The updated users', type: UserGetDTO }) - @ApiNotOkResponses({ 400: 'Invalid input', 404: 'User(s) not found' }) - async update(@Req() req: Request, @Body() input: UserPatchDTO[]): Promise { - const schema = z - .object({ - id: z.coerce.number(), - email: z.string().email().optional(), - password: z.string().optional(), - birth_date: z.string().datetime().optional(), - first_name: z.string().optional(), - last_name: z.string().optional(), - nickname: z.string().optional(), - gender: z.enum(USER_GENDER).optional(), - pronouns: z.string().optional(), - secondary_email: z.string().email().optional(), - phone: z.string().optional(), - parent_contact: z.string().optional(), - // TODO: (KEY: 1) Make a PR to implement cursus & specialty in the API - // cursus: z.string().optional(), - // specialty: z.string().optional(), - promotion: z.coerce.number().optional(), - }) - .strict(); - - validate(z.array(schema).min(1), input); - return this.usersService.update((req.user as User).id, input); + @ApiOperation({ summary: 'Update your account' }) + @ApiParam({ name: 'id', description: 'Your user ID' }) + @ApiOkResponse({ description: 'User data', type: OutputUserDTO }) + @ApiNotOkResponses({ 400: 'Invalid ID or input', 404: 'User not found' }) + async updateSelf( + @Req() req: Request, + @Param() params: InputIdParamDTO, + @Body() body: InputUpdateUserDTO, + ): Promise { + return this.usersService.update(req.user.id, params.id, body); } @Delete(':id') @@ -91,12 +57,10 @@ export class UsersDataController { @GuardSelfParam('id') @ApiOperation({ summary: 'Delete your account' }) @ApiParam({ name: 'id', description: 'Your user ID' }) - @ApiOkResponse({ description: 'User deleted', type: MessageResponseDTO }) + @ApiOkResponse({ description: 'User deleted', type: OutputMessageDTO }) @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) - async delete(@Param('id') id: number): Promise { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - - return this.usersService.delete(id); + async delete(@Param() params: InputIdParamDTO): Promise { + return this.usersService.delete(params.id); } @Get(':id/data') @@ -104,25 +68,24 @@ export class UsersDataController { @GuardSelfOrPermissions('id', ['CAN_READ_USER_PRIVATE']) @ApiOperation({ summary: 'Get all information of a user' }) @ApiParam({ name: 'id', description: 'The user ID' }) - @ApiOkResponse({ description: 'User data', type: UserGetDTO }) + @ApiOkResponse({ description: 'User data', type: OutputUserDTO }) @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) - async getPrivate(@Param('id') id: number): Promise { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - - return await this.usersService.findOne(id, false); + async getPrivate(@Param() params: InputIdParamDTO): Promise { + return this.usersService.findOne(params.id, false); } @Get(':id/data/public') - @UseGuards(SelfOrPermsOrSubGuard) - @GuardSelfOrPermsOrSub('id', ['CAN_READ_USER']) + @UseGuards(SelfOrPermissionGuard) + @GuardSelfOrPermissions('id', ['CAN_READ_USER']) @ApiOperation({ summary: 'Get publicly available information of a user' }) @ApiParam({ name: 'id', description: 'The user ID' }) - @ApiOkResponse({ description: 'User data, excepted privates fields (set in the visibility table)', type: UserGetDTO }) + @ApiOkResponse({ + description: 'User data, excepted privates fields (set in the visibility table)', + type: OutputUserDTO, + }) @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) - async getPublic(@Param('id') id: number): Promise { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - - return this.usersService.findOne(id); + async getPublic(@Param() params: InputIdParamDTO): Promise { + return this.usersService.findOne(params.id, true); } @Get(':id/data/visibility') @@ -130,12 +93,10 @@ export class UsersDataController { @GuardSelfOrPermissions('id', ['CAN_READ_USER_PRIVATE']) @ApiOperation({ summary: 'Get visibility settings of a user' }) @ApiParam({ name: 'id', description: 'The user ID' }) - @ApiOkResponse({ description: 'User data', type: UserVisibilityGetDTO }) + @ApiOkResponse({ description: 'User data', type: OutputUserVisibilityDTO }) @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) - async getVisibility(@Param('id') id: number): Promise { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - - return (await this.usersService.findVisibilities(id))[0]; + async getVisibility(@Param() params: InputIdParamDTO): Promise { + return (await this.usersService.findVisibilities(params.id))[0]; } @Patch(':id/data/visibility') @@ -143,29 +104,13 @@ export class UsersDataController { @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Update visibility settings of a user' }) @ApiParam({ name: 'id', description: 'The user ID' }) - @ApiOkResponse({ description: 'User data', type: UserVisibilityGetDTO }) + @ApiOkResponse({ description: 'User data', type: OutputUserVisibilityDTO }) @ApiNotOkResponses({ 400: 'Invalid ID or input', 404: 'User not found' }) async updateVisibility( - @Param('id') id: number, - @Body() input: UserVisibilityPatchDTO, - ): Promise { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - - const schema = z - .object({ - email: z.boolean(), - secondary_email: z.boolean(), - birth_date: z.boolean(), - gender: z.boolean(), - pronouns: z.boolean(), - promotion: z.boolean(), - phone: z.boolean(), - parent_contact: z.boolean(), - }) - .strict(); - validate(schema, input); - - return this.usersService.updateVisibility(id, input); + @Param() params: InputIdParamDTO, + @Body() input: InputUpdateUserVisibilityDTO, + ): Promise { + return this.usersService.updateVisibility(params.id, input); } @Get(':id/roles') @@ -173,12 +118,10 @@ export class UsersDataController { @GuardSelfOrPermissions('id', ['CAN_READ_USER', 'CAN_READ_ROLE']) @ApiOperation({ summary: 'Get roles of a user' }) @ApiParam({ name: 'id', description: 'The user ID' }) - @ApiOkResponse({ description: 'Roles of the user', type: [UserRoleGetDTO] }) + @ApiOkResponse({ description: 'Roles of the user', type: [OutputUserRoleDTO] }) @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) - async getUserRoles(@Param('id') id: number): Promise { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - - return this.usersService.getUserRoles(id, { show_expired: true, show_revoked: true }); + async getUserRoles(@Param() params: InputIdParamDTO): Promise { + return this.usersService.getUserRoles(params.id, { show_expired: true, show_revoked: true }); } @Get(':id/permissions') @@ -186,11 +129,9 @@ export class UsersDataController { @GuardSelfOrPermissions('id', ['CAN_READ_USER', 'CAN_READ_PERMISSIONS_OF_USER']) @ApiOperation({ summary: 'Get permissions of a user' }) @ApiParam({ name: 'id', description: 'The user ID' }) - @ApiOkResponse({ description: 'Permissions of the user', type: [PermissionGetDTO] }) + @ApiOkResponse({ description: 'Permissions of the user', type: [OutputPermissionDTO] }) @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) - async getUserPermissions(@Param('id') id: number): Promise { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - - return this.usersService.getUserPermissions(id, { show_expired: true, show_revoked: true }); + async getUserPermissions(@Param() params: InputIdParamDTO): Promise { + return this.usersService.getUserPermissions(params.id, { show_expired: true, show_revoked: true }); } } diff --git a/src/modules/users/controllers/users-files.controller.ts b/src/modules/users/controllers/users-files.controller.ts index 345faf56..c0c968a3 100644 --- a/src/modules/users/controllers/users-files.controller.ts +++ b/src/modules/users/controllers/users-files.controller.ts @@ -1,36 +1,21 @@ -import { - BadRequestException, - Controller, - Delete, - Get, - Param, - Post, - Req, - StreamableFile, - UnauthorizedException, - UploadedFile, - UseGuards, -} from '@nestjs/common'; +import { Controller, Delete, Get, Param, Post, Req, UploadedFile, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { z } from 'zod'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/error.decorator'; -import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; +import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/_mixin/dto/input.dto'; +import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { i18nBadRequestException } from '@modules/_mixin/http-errors'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; -import { GuardSelfOrPermsOrSub } from '@modules/auth/decorators/self-or-sub-perms.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { SelfOrPermissionGuard } from '@modules/auth/guards/self-or-perms.guard'; -import { SelfOrPermsOrSubGuard } from '@modules/auth/guards/self-or-sub-or-perms.guard'; import { ApiDownloadFile } from '@modules/files/decorators/download.decorator'; import { ApiUploadFile } from '@modules/files/decorators/upload.decorator'; import { FilesService } from '@modules/files/files.service'; -import { TranslateService } from '@modules/translate/translate.service'; -import { validate } from '@utils/validate'; -import { UserGetBannerDTO, UserGetPictureDTO } from '../dto/get.dto'; -import { Request, User } from '../entities/user.entity'; +import { OutputUserBannerDTO, OutputUserPictureDTO } from '../dto/output.dto'; +import { Request } from '../entities/user.entity'; import { UsersFilesService } from '../services/users-files.service'; @ApiTags('Users Files') @@ -38,29 +23,23 @@ import { UsersFilesService } from '../services/users-files.service'; @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() export class UsersFilesController { - constructor( - private readonly t: TranslateService, - private readonly usersFilesService: UsersFilesService, - private readonly filesService: FilesService, - ) {} + constructor(private readonly usersFilesService: UsersFilesService, private readonly filesService: FilesService) {} @Post(':id/picture') @UseGuards(SelfOrPermissionGuard) @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Update user profile picture' }) @ApiParam({ name: 'id', description: 'The user ID' }) - @ApiOkResponse({ description: 'The updated user picture', type: UserGetPictureDTO }) + @ApiOkResponse({ description: 'The updated user picture', type: OutputUserPictureDTO }) @ApiNotOkResponses({ 400: 'Invalid user ID or missing uploaded file', 404: 'User not found' }) @ApiUploadFile() async editPicture( @Req() req: Request, + @Param() params: InputIdParamDTO, @UploadedFile() file: Express.Multer.File, - @Param('id') id: number, - ): Promise { - if (!file) throw new BadRequestException(this.t.Errors.File.NotProvided()); - validate(z.coerce.number().int().min(1), id); - - return this.usersFilesService.updatePicture(req.user, id, file); + ): Promise { + if (!file) throw new i18nBadRequestException('validations.file.invalid.not_provided'); + return this.usersFilesService.updatePicture(req.user, params.id, file); } @Delete(':id/picture') @@ -68,34 +47,20 @@ export class UsersFilesController { @GuardPermissions('CAN_EDIT_USER') @ApiOperation({ summary: 'Delete user profile picture' }) @ApiParam({ name: 'id', description: 'The user ID' }) - @ApiOkResponse({ type: MessageResponseDTO }) + @ApiOkResponse({ type: OutputMessageDTO }) @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) - async deletePicture(@Param('id') id: number): Promise { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - - return this.usersFilesService.deletePicture(id); + async deletePicture(@Param() params: InputIdParamDTO): Promise { + return this.usersFilesService.deletePicture(params.id); } @Get(':id/picture') - @UseGuards(SelfOrPermsOrSubGuard) - @GuardSelfOrPermsOrSub('id', ['CAN_READ_USER']) @ApiOperation({ summary: 'Get user profile picture' }) @ApiParam({ name: 'id', description: 'The user ID' }) @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) @ApiDownloadFile() - async getPicture(@Req() req: Request, @Param('id') id: number) { - validate(z.coerce.number().int().min(1), id); - - const picture = await this.usersFilesService.getPicture(id); - await picture.visibility?.init(); - - if (await this.filesService.canReadFile(picture, req.user)) - return new StreamableFile(this.filesService.toReadable(picture)); - - // Should not happen unless the user is subscribed but not in the visibility group of subscribers - // -> all others options are caught by the guard - /* istanbul ignore next-line */ - throw new UnauthorizedException(this.t.Errors.File.Unauthorized(picture.visibility?.name)); + async getPicture(@Req() req: Request, @Param() params: InputIdParamDTO) { + const picture = await this.usersFilesService.getPicture(params.id); + return this.filesService.getAsStreamable(picture, req.user.id); } @Post(':id/banner') @@ -103,14 +68,15 @@ export class UsersFilesController { @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Update user profile banner' }) @ApiParam({ name: 'id', description: 'The user ID' }) - @ApiOkResponse({ description: 'The updated user banner', type: UserGetBannerDTO }) + @ApiOkResponse({ description: 'The updated user banner', type: OutputUserBannerDTO }) @ApiNotOkResponses({ 400: 'Invalid user ID or missing uploaded file', 404: 'User not found' }) @ApiUploadFile() - async editBanner(@UploadedFile() file: Express.Multer.File, @Param('id') id: number): Promise { - if (!file) throw new BadRequestException(this.t.Errors.File.NotProvided()); - validate(z.coerce.number().int().min(1), id); - - return this.usersFilesService.updateBanner(id, file); + async editBanner( + @Param() params: InputIdParamDTO, + @UploadedFile() file: Express.Multer.File, + ): Promise { + if (!file) throw new i18nBadRequestException('validations.file.invalid.not_provided'); + return this.usersFilesService.updateBanner(params.id, file); } @Delete(':id/banner') @@ -118,33 +84,19 @@ export class UsersFilesController { @GuardSelfOrPermissions('id', ['CAN_EDIT_USER']) @ApiOperation({ summary: 'Delete user profile banner' }) @ApiParam({ name: 'id', description: 'The user ID' }) - @ApiOkResponse({ type: MessageResponseDTO }) + @ApiOkResponse({ type: OutputMessageDTO }) @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) - async deleteBanner(@Param('id') id: number): Promise { - validate(z.coerce.number().int().min(1), id, this.t.Errors.Id.Invalid(User, id)); - - return this.usersFilesService.deleteBanner(id); + async deleteBanner(@Param() params: InputIdParamDTO): Promise { + return this.usersFilesService.deleteBanner(params.id); } @Get(':id/banner') - @UseGuards(SelfOrPermsOrSubGuard) - @GuardSelfOrPermsOrSub('id', ['CAN_READ_USER']) @ApiOperation({ summary: 'Get user profile banner' }) @ApiParam({ name: 'id', description: 'The user ID' }) @ApiNotOkResponses({ 400: 'Invalid ID', 404: 'User not found' }) @ApiDownloadFile() - async getBanner(@Req() req: Request, @Param('id') id: number) { - validate(z.coerce.number().int().min(1), id); - - const banner = await this.usersFilesService.getBanner(id); - await banner.visibility?.init(); - - if (await this.filesService.canReadFile(banner, req.user)) - return new StreamableFile(this.filesService.toReadable(banner)); - - // Should not happen unless the user is subscribed but not in the visibility group of subscribers - // -> all others options are caught by the guard - /* istanbul ignore next-line */ - throw new UnauthorizedException(this.t.Errors.File.Unauthorized(banner.visibility?.name)); + async getBanner(@Req() req: Request, @Param() params: InputIdParamDTO) { + const banner = await this.usersFilesService.getBanner(params.id); + return this.filesService.getAsStreamable(banner, req.user.id); } } diff --git a/src/modules/users/dto/base-user.dto.ts b/src/modules/users/dto/base-user.dto.ts deleted file mode 100644 index bce3e703..00000000 --- a/src/modules/users/dto/base-user.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { IBaseUserDTO } from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; - -import { BaseResponseDTO } from '@modules/_mixin/dto/base.dto'; - -export class BaseUserResponseDTO extends BaseResponseDTO implements IBaseUserDTO { - @ApiProperty() - first_name: string; - - @ApiProperty() - last_name: string; - - @ApiProperty({ required: false }) - nickname?: string; -} diff --git a/src/modules/users/dto/get.dto.ts b/src/modules/users/dto/get.dto.ts deleted file mode 100644 index 2970a687..00000000 --- a/src/modules/users/dto/get.dto.ts +++ /dev/null @@ -1,263 +0,0 @@ -import type { email } from '#types'; -import type { - IUserBannerResponseDTO, - IUserGetPrivateDTO, - IUserPictureResponseDTO, - IUserRoleGetDTO, - IUserVisibilityGetDTO, -} from '#types/api'; - -import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsDate, IsEmail, IsIn, IsInt, IsNumber, IsString } from 'class-validator'; - -import { IUserGetDTO, PERMISSION_NAMES } from '#types/api'; -import { USER_GENDER, genders } from '@exported/api/constants/genders'; -import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -import { BaseResponseDTO } from '@modules/_mixin/dto/base.dto'; -import { FileGetDTO } from '@modules/files/dto/get.dto'; - -export class UserRoleGetDTO implements IUserRoleGetDTO { - @ApiProperty({ required: true, minimum: 1 }) - @IsInt() - id: number; - - @ApiProperty({ required: true, type: Date }) - @IsDate() - created: Date; - - @ApiProperty({ required: true, type: Date }) - @IsDate() - updated: Date; - - @ApiProperty({ required: true, type: Date }) - @IsDate() - expires: Date; - - @ApiProperty({ required: true, example: 'AE_ADMINS' }) - @IsString() - name: Uppercase; - - @ApiProperty({ required: true, type: Boolean, default: false }) - @IsBoolean() - revoked: boolean; - - @ApiProperty({ enum: PERMISSIONS_NAMES, isArray: true }) - @IsString() - permissions: Array; -} - -export class UserGetDTO extends BaseResponseDTO implements IUserGetDTO { - @ApiProperty({ example: 'John' }) - @IsString() - first_name: string; - - @ApiProperty({ example: 'Doe' }) - @IsString() - last_name: string; - - @ApiProperty({ example: 'John Doe' }) - @IsString() - full_name: string; - - @ApiProperty({ minimum: 1 }) - @IsNumber() - picture?: number; - - @ApiProperty({ minimum: 1 }) - @IsNumber() - banner?: number; - - @ApiProperty({ type: String, example: 'example@domain.com' }) - @IsEmail() - email?: email; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - email_verified?: boolean; - - @ApiProperty({ example: new Date('1999-12-31').toISOString() }) - @IsDate() - birth_date?: Date; - - @ApiProperty({ example: 21 }) - @IsNumber() - age: number; - - @ApiProperty() - @IsBoolean() - is_minor: boolean; - - @ApiProperty() - @IsString() - nickname?: string; - - @ApiProperty({ example: USER_GENDER[0], enum: USER_GENDER }) - @IsString() - @IsIn(USER_GENDER) - gender?: genders; - - @ApiProperty({ example: null }) - @IsString() - pronouns?: string; - - @ApiProperty({ type: Number, minimum: 1 }) - @IsNumber() - promotion?: number; - - @ApiProperty({ example: new Date().toISOString() }) - @IsDate() - last_seen?: Date; - - @ApiProperty({ example: false }) - @IsBoolean() - subscribed: boolean; // TODO: (KEY: 2) Make a PR to implement subscriptions in the API - - @ApiProperty() - @IsEmail() - secondary_email?: string; - - @ApiProperty() - @IsString() - phone?: string; - - @ApiProperty() - @IsString() - parent_contact?: string; - - @ApiProperty({ type: Date }) - @IsDate() - verified?: Date; -} - -export class UserGetPrivateDTO extends BaseResponseDTO implements IUserGetPrivateDTO { - @ApiProperty({ example: 'John' }) - @IsString() - first_name: string; - - @ApiProperty({ example: 'Doe' }) - @IsString() - last_name: string; - - @ApiProperty({ example: 'John Doe' }) - @IsString() - full_name: string; - - @ApiProperty({ minimum: 1 }) - @IsNumber() - picture?: number; - - @ApiProperty({ minimum: 1 }) - @IsNumber() - banner?: number; - - @ApiProperty({ type: String, example: 'example@domain.com' }) - @IsEmail() - email: email; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - email_verified?: boolean; - - @ApiProperty({ example: new Date('1999-12-31').toISOString() }) - @IsDate() - birth_date: Date; - - @ApiProperty({ example: 21 }) - @IsNumber() - age: number; - - @ApiProperty() - @IsBoolean() - is_minor: boolean; - - @ApiProperty() - @IsString() - nickname?: string; - - @ApiProperty({ example: USER_GENDER[0], enum: USER_GENDER }) - @IsString() - @IsIn(USER_GENDER) - gender: genders; - - @ApiProperty({ example: null }) - @IsString() - pronouns: string; - - @ApiProperty({ type: Number, minimum: 1 }) - @IsNumber() - promotion: number; - - @ApiProperty({ example: new Date().toISOString() }) - @IsDate() - last_seen?: Date; - - @ApiProperty({ example: false }) - @IsBoolean() - subscribed: boolean; // TODO: (KEY: 2) Make a PR to implement subscriptions in the API - - @ApiProperty() - @IsEmail() - secondary_email: string; - - @ApiProperty() - @IsString() - phone: string; - - @ApiProperty() - @IsString() - parent_contact: string; - - @ApiProperty({ type: Date }) - @IsDate() - verified?: Date; -} - -export class UserVisibilityGetDTO implements IUserVisibilityGetDTO { - @ApiProperty({ minimum: 1 }) - @IsInt() - user_id: number; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - email: boolean; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - secondary_email: boolean; - - @ApiProperty({ type: Boolean, default: true }) - @IsBoolean() - birth_date: boolean; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - gender: boolean; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - pronouns: boolean; - - @ApiProperty({ type: Boolean, default: true }) - @IsBoolean() - promotion: boolean; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - phone: boolean; - - @ApiProperty({ type: Boolean, default: false }) - @IsBoolean() - parent_contact: boolean; -} - -export class UserGetPictureDTO extends FileGetDTO implements IUserPictureResponseDTO { - @ApiProperty({ minimum: 1 }) - @IsInt() - picture_user_id: number; -} - -export class UserGetBannerDTO extends FileGetDTO implements IUserBannerResponseDTO { - @ApiProperty({ minimum: 1 }) - @IsInt() - banner_user_id: number; -} diff --git a/src/modules/users/dto/input.dto.ts b/src/modules/users/dto/input.dto.ts new file mode 100644 index 00000000..63167f04 --- /dev/null +++ b/src/modules/users/dto/input.dto.ts @@ -0,0 +1,106 @@ +import type { email } from '#types'; +import type { GENDERS, InputUpdateUserVisibilityDto, InputUpdateUserDto, I18nTranslations } from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsIn, IsOptional } from 'class-validator'; +import { i18nValidationMessage } from 'nestjs-i18n'; + +import { USER_GENDER } from '@exported/api/constants/genders'; +import { + I18nIsBoolean, + I18nIsDate, + I18nIsEmail, + I18nIsId, + I18nIsPhoneNumber, + I18nIsString, +} from '@modules/_mixin/decorators'; + +export class InputUpdateUserDTO implements InputUpdateUserDto { + @ApiProperty({ required: false }) + @I18nIsEmail() + @IsOptional() + email?: email; + + @ApiProperty({ required: false }) + @I18nIsDate() + @IsOptional() + birth_date?: Date; + + @ApiProperty({ required: false }) + @I18nIsString() + @IsOptional() + first_name?: string; + + @ApiProperty({ required: false }) + @I18nIsString() + @IsOptional() + last_name?: string; + + @ApiProperty({ required: false }) + @IsIn(USER_GENDER, { + message: i18nValidationMessage('validations.gender.invalid.format', { + genders: USER_GENDER.join("', '"), + }), + }) + @IsOptional() + gender?: GENDERS; + + @ApiProperty({ required: false }) + @I18nIsString() + @IsOptional() + pronouns?: string; + + @ApiProperty({ required: false, minimum: 1 }) + @I18nIsId() + @IsOptional() + promotion?: number; + + @ApiProperty({ required: false }) + @I18nIsEmail() + @IsOptional() + secondary_email?: email; + + @ApiProperty({ required: false }) + @I18nIsPhoneNumber() + @IsOptional() + phone?: string; + + @ApiProperty({ required: false }) + @I18nIsPhoneNumber() + @IsOptional() + parents_phone?: string; +} + +export class InputUpdateUserVisibilityDTO implements InputUpdateUserVisibilityDto { + @ApiProperty() + @I18nIsBoolean() + email: boolean; + + @ApiProperty() + @I18nIsBoolean() + birth_date: boolean; + + @ApiProperty() + @I18nIsBoolean() + gender: boolean; + + @ApiProperty() + @I18nIsBoolean() + pronouns: boolean; + + @ApiProperty() + @I18nIsBoolean() + promotion: boolean; + + @ApiProperty() + @I18nIsBoolean() + secondary_email: boolean; + + @ApiProperty() + @I18nIsBoolean() + phone: boolean; + + @ApiProperty() + @I18nIsBoolean() + parents_phone: boolean; +} diff --git a/src/modules/users/dto/output.dto.ts b/src/modules/users/dto/output.dto.ts new file mode 100644 index 00000000..cd4d6ccc --- /dev/null +++ b/src/modules/users/dto/output.dto.ts @@ -0,0 +1,142 @@ +import type { email } from '#types'; +import type { + OutputUserBannerDto, + OutputUserPictureDto, + OutputUserRoleDto, + OutputUserVisibilityDto, + OutputBaseUserDto, +} from '#types/api'; + +import { ApiProperty } from '@nestjs/swagger'; + +import { OutputUserDto, PERMISSION_NAMES, GENDERS } from '#types/api'; +import { USER_GENDER } from '@exported/api/constants/genders'; +import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; +import { OutputBaseDTO } from '@modules/_mixin/dto/output.dto'; +import { OutputFileDTO } from '@modules/files/dto/output.dto'; + +export class OutputBaseUserDTO extends OutputBaseDTO implements OutputBaseUserDto { + @ApiProperty() + first_name: string; + + @ApiProperty() + last_name: string; + + @ApiProperty({ required: false }) + nickname?: string; +} + +export class OutputUserRoleDTO extends OutputBaseDTO implements OutputUserRoleDto { + @ApiProperty({ required: true, type: Date, example: new Date().toISOString() }) + expires: Date; + + @ApiProperty({ required: true, example: 'AE_ADMINS' }) + name: Uppercase; + + @ApiProperty({ required: true, type: Boolean, default: false }) + revoked: boolean; + + @ApiProperty({ enum: PERMISSIONS_NAMES, isArray: true }) + permissions: Array; +} + +export class OutputUserDTO extends OutputBaseDTO implements OutputUserDto { + @ApiProperty({ example: 'John' }) + first_name: string; + + @ApiProperty({ example: 'Doe' }) + last_name: string; + + @ApiProperty({ example: 'John Doe' }) + full_name: string; + + @ApiProperty({ minimum: 1, required: false }) + picture?: number; + + @ApiProperty({ minimum: 1, required: false }) + banner?: number; + + @ApiProperty({ type: String, example: 'example@domain.com', required: false }) + email?: email; + + @ApiProperty({ type: Boolean, default: false }) + email_verified?: boolean; + + @ApiProperty({ example: new Date('1999-12-31').toISOString() }) + birth_date?: Date; + + @ApiProperty({ example: 21 }) + age: number; + + @ApiProperty() + is_minor: boolean; + + @ApiProperty({ required: false }) + nickname?: string; + + @ApiProperty({ example: USER_GENDER[0], enum: USER_GENDER }) + gender?: GENDERS; + + @ApiProperty({ example: null }) + pronouns?: string; + + @ApiProperty({ minimum: 1 }) + promotion?: number; + + @ApiProperty({ example: new Date().toISOString() }) + last_seen?: Date; + + @ApiProperty({ example: false }) + subscribed: boolean; // TODO: (KEY: 2) Make a PR to implement subscriptions in the API + + @ApiProperty({ required: false }) + secondary_email?: string; + + @ApiProperty({ required: false }) + phone?: string; + + @ApiProperty({ required: false }) + parents_phone?: string; + + @ApiProperty({ type: Date, example: new Date().toISOString(), required: false }) + verified?: Date; +} + +export class OutputUserVisibilityDTO implements OutputUserVisibilityDto { + @ApiProperty({ minimum: 1 }) + user_id: number; + + @ApiProperty({ type: Boolean, default: false }) + email: boolean; + + @ApiProperty({ type: Boolean, default: false }) + secondary_email: boolean; + + @ApiProperty({ type: Boolean, default: true }) + birth_date: boolean; + + @ApiProperty({ type: Boolean, default: false }) + gender: boolean; + + @ApiProperty({ type: Boolean, default: false }) + pronouns: boolean; + + @ApiProperty({ type: Boolean, default: true }) + promotion: boolean; + + @ApiProperty({ type: Boolean, default: false }) + phone: boolean; + + @ApiProperty({ type: Boolean, default: false }) + parents_phone: boolean; +} + +export class OutputUserPictureDTO extends OutputFileDTO implements OutputUserPictureDto { + @ApiProperty({ minimum: 1 }) + picture_user_id: number; +} + +export class OutputUserBannerDTO extends OutputFileDTO implements OutputUserBannerDto { + @ApiProperty({ minimum: 1 }) + banner_user_id: number; +} diff --git a/src/modules/users/dto/patch.dto.ts b/src/modules/users/dto/patch.dto.ts deleted file mode 100644 index edc94918..00000000 --- a/src/modules/users/dto/patch.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { IUserVisibilityPatchDTO, IUserPatchDTO } from '#types/api'; - -import { UserGetDTO, UserVisibilityGetDTO } from './get.dto'; - -export class UserPatchDTO extends UserGetDTO implements IUserPatchDTO {} -export class UserVisibilityPatchDTO extends UserVisibilityGetDTO implements IUserVisibilityPatchDTO {} diff --git a/src/modules/users/entities/user-visibility.entity.ts b/src/modules/users/entities/user-visibility.entity.ts index dfe0dfa5..8debbaaf 100644 --- a/src/modules/users/entities/user-visibility.entity.ts +++ b/src/modules/users/entities/user-visibility.entity.ts @@ -56,5 +56,5 @@ export class UserVisibility extends BaseEntity { /** Wether the user parent's contact should be visible or not */ @Property({ onCreate: () => false }) - parent_contact: boolean; + parents_phone: boolean; } diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 398b3984..73912330 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -12,13 +12,6 @@ import { Role } from '@modules/roles/entities/role.entity'; import { UserBanner } from './user-banner.entity'; import { UserPicture } from './user-picture.entity'; -import { UserVisibility } from './user-visibility.entity'; - -/** Keys that may change of visibility */ -export type UserPrivateKeys = Omit; - -export type UserPrivate = User; -export type UserPublic = Omit & Partial>; export type Request = Express.Request & { user: User; @@ -119,7 +112,7 @@ export class User extends BaseEntity { phone?: string; @Property({ nullable: true }) - parent_contact?: string; + parents_phone?: string; //* PERMISSIONS & TRACKING @OneToMany(() => Permission, (permission) => permission.user, { diff --git a/src/modules/users/services/users-data.service.ts b/src/modules/users/services/users-data.service.ts index 39c18b3e..6923ffdc 100644 --- a/src/modules/users/services/users-data.service.ts +++ b/src/modules/users/services/users-data.service.ts @@ -2,33 +2,31 @@ import type { email } from '#types'; import type { I18nTranslations, PERMISSION_NAMES } from '#types/api'; import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; -import { BadRequestException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { compareSync, hashSync } from 'bcrypt'; import { I18nContext, I18nService } from 'nestjs-i18n'; import { z } from 'zod'; import { env } from '@env'; -import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; -import { CreateUserDTO, UserPostDTO } from '@modules/auth/dto/post.dto'; +import { generateRandomPassword, isStrongPassword } from '@modules/_mixin/decorators'; +import { OutputCreatedDTO, OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; +import { InputRegisterUserAdminDTO, InputRegisterUserDTO } from '@modules/auth/dto/input.dto'; import { EmailsService } from '@modules/emails/emails.service'; -import { PermissionGetDTO } from '@modules/permissions/dto/get.dto'; +import { OutputPermissionDTO } from '@modules/permissions/dto/output.dto'; import { RoleExpiration } from '@modules/roles/entities/role-expiration.entity'; -import { TranslateService } from '@modules/translate/translate.service'; import { UserVisibility } from '@modules/users/entities/user-visibility.entity'; -import { User, UserPrivate } from '@modules/users/entities/user.entity'; +import { User } from '@modules/users/entities/user.entity'; import { checkBirthDate } from '@utils/dates'; -import { checkPasswordStrength, generateRandomPassword } from '@utils/password'; import { getTemplate } from '@utils/template'; -import { BaseUserResponseDTO } from '../dto/base-user.dto'; -import { UserGetDTO, UserGetPrivateDTO, UserRoleGetDTO, UserVisibilityGetDTO } from '../dto/get.dto'; -import { UserPatchDTO, UserVisibilityPatchDTO } from '../dto/patch.dto'; +import { InputUpdateUserDTO, InputUpdateUserVisibilityDTO } from '../dto/input.dto'; +import { OutputUserDTO, OutputBaseUserDTO, OutputUserRoleDTO, OutputUserVisibilityDTO } from '../dto/output.dto'; @Injectable() export class UsersDataService { constructor( - private readonly t: TranslateService, private readonly orm: MikroORM, private readonly i18n: I18nService, private readonly emailsService: EmailsService, @@ -58,15 +56,15 @@ export class UsersDataService { * @returns {Promise} The filtered users */ @CreateRequestContext() - async sanitize(users: User[]): Promise { - const res: UserGetDTO[] = []; + async sanitize(users: User[]): Promise { + const res: OutputUserDTO[] = []; const visibilities = await this.findVisibilities(users.map((u) => u.id)); visibilities.forEach((v) => { - const user = users.find((u) => u.id === v.user_id).toObject() as unknown as UserGetDTO; + const user = users.find((u) => u.id === v.user_id).toObject() as unknown as OutputUserDTO; Object.entries(v).forEach(([key, val]) => { - const key_ = key as keyof Omit; + const key_ = key as keyof Omit; if (val === false) delete user[key_]; }); @@ -79,10 +77,10 @@ export class UsersDataService { /** * Keep only the base fields of a user * @param {User[]} users the users to filter - * @returns {BaseUserResponseDTO[]} The "filtered" users + * @returns {OutputBaseUserDTO[]} The "filtered" users */ - asBaseUsers(users: User[]): BaseUserResponseDTO[] { - const res: BaseUserResponseDTO[] = []; + asBaseUsers(users: User[]): OutputBaseUserDTO[] { + const res: OutputBaseUserDTO[] = []; for (const user of users.sort((a, b) => a.id - b.id)) { res.push({ id: user.id, @@ -99,10 +97,10 @@ export class UsersDataService { /** * Find a user by id or email and return it - * @param {Partial} id_or_email The id or email of the user to find + * @param {number | email} id_or_email The id or email of the user to find * @param {boolean} filter Whether to filter the user or not (default: true) * - * @returns {Promise} The user found (public if filter is true) + * @returns {Promise} The user found (public if filter is true) * @throws {BadRequestException} If no id or email is provided * @throws {NotFoundException} If no user is found with the provided id/email * @@ -112,58 +110,57 @@ export class UsersDataService { * const user2: UserGetDTO = await this.usersService.findOne({ email: 'example@domain.com' }); * ``` */ - async findOne(id_or_email: number | email, filter: false): Promise; - async findOne(id_or_email: number | email): Promise; - @CreateRequestContext() - async findOne(id_or_email: number | email, filter = true): Promise { + async findOne(id_or_email: number | email, filter: boolean): Promise { let user: User = null; const parsed = z.union([z.coerce.number(), z.string().email()]).parse(id_or_email); if (typeof parsed === 'number') user = await this.orm.em.findOne(User, { id: parsed }); else if (typeof parsed === 'string') user = await this.orm.em.findOne(User, { email: parsed as email }); - if (!user && typeof parsed === 'number') throw new NotFoundException(this.t.Errors.Id.NotFound(User, parsed)); + if (!user && typeof parsed === 'number') + throw new i18nNotFoundException('validations.user.not_found.id', { id: parsed }); + if (!user && typeof parsed === 'string') - throw new NotFoundException(this.t.Errors.Email.NotFound(User, parsed as email)); + throw new i18nNotFoundException('validations.user.not_found.email', { email: parsed }); - return filter ? (await this.sanitize([user]))[0] : user; + if (filter) return (await this.sanitize([user]))[0]; + return user.toObject() as unknown as OutputUserDTO; } /** * Return the visibility parameters of given users * @param {number} ids The ids of the users - * @returns {Promise} The visibility parameters of each user + * @returns {Promise} The visibility parameters of each user */ @CreateRequestContext() - async findVisibilities(ids: number[] | number): Promise { + async findVisibilities(ids: number[] | number): Promise { if (!Array.isArray(ids)) ids = [ids]; const users = await this.orm.em.find(User, { id: { $in: ids } }); - if (!users || users.length === 0) throw new NotFoundException(this.t.Errors.Id.NotFounds(User, ids)); + if (!users || users.length === 0) + if (ids.length === 1) throw new i18nNotFoundException('validations.user.not_found.id', { id: ids[0] }); + else throw new i18nNotFoundException('validations.users.not_found.ids', { ids: ids.join("', '") }); const visibilities = await this.orm.em.find(UserVisibility, { user: { $in: users } }); - return visibilities.map((v) => v.toObject() as unknown as UserVisibilityGetDTO); + return visibilities.map((v) => v.toObject() as unknown as OutputUserVisibilityDTO); } @CreateRequestContext() - async updateVisibility(id: number, input: UserVisibilityPatchDTO): Promise { + async updateVisibility(id: number, input: InputUpdateUserVisibilityDTO): Promise { const user = await this.orm.em.findOne(User, { id }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id }); const visibility = await this.orm.em.findOne(UserVisibility, { user }); - // Should never happen as the visibility is created when the user is created - /* istanbul ignore next-line */ - if (!visibility) throw new NotFoundException(this.t.Errors.Id.NotFound(UserVisibility, id)); Object.assign(visibility, input); await this.orm.em.persistAndFlush(visibility); - return visibility.toObject() as unknown as UserVisibilityGetDTO; + return visibility.toObject() as unknown as OutputUserVisibilityDTO; } @CreateRequestContext() - async register(input: UserPostDTO): Promise { + async register(input: InputRegisterUserDTO): Promise { Object.entries(input).forEach(([key, value]) => { if (typeof value === 'string') { // @ts-ignore @@ -172,12 +169,12 @@ export class UsersDataService { }); if (await this.orm.em.findOne(User, { email: input.email })) - throw new BadRequestException(this.t.Errors.Email.IsAlreadyUsed(input.email)); + throw new i18nBadRequestException('validations.email.invalid.used', { email: input.email }); if (!checkBirthDate(input.birth_date)) - throw new BadRequestException(this.t.Errors.BirthDate.Invalid(input.birth_date)); + throw new i18nBadRequestException('validations.birth_date.invalid.outbound', { date: input.birth_date }); - if (!checkPasswordStrength(input.password)) throw new BadRequestException(this.t.Errors.Password.Weak()); + if (!isStrongPassword(input.password)) throw new i18nBadRequestException('validations.password.invalid.weak'); // Check if the password is already hashed if (input.password.length !== 60) input.password = hashSync(input.password, 10); @@ -192,7 +189,7 @@ export class UsersDataService { // Save changes to the database & create the user's visibility parameters this.orm.em.create(UserVisibility, { user }); - return { message: this.t.Success.User.Registered(), statusCode: 201 }; + return new OutputCreatedDTO('validations.user.success.registered', { name: registered.full_name }); } @CreateRequestContext() @@ -204,7 +201,7 @@ export class UsersDataService { const user_b = await this.orm.em.findOne(User, { email }); // Check if the email is already used by someone else - if (user_b && user_b.id !== user.id) throw new BadRequestException(this.t.Errors.Email.IsAlreadyUsed(email)); + if (user_b && user_b.id !== user.id) throw new i18nBadRequestException('validations.email.invalid.used', { email }); const email_token = generateRandomPassword(12); user.email_verification = hashSync(email_token, 10); @@ -236,20 +233,23 @@ export class UsersDataService { } @CreateRequestContext() - async registerByAdmin(inputs: CreateUserDTO[]): Promise { + async registerByAdmin(inputs: InputRegisterUserAdminDTO[]): Promise { const existing_users = await this.orm.em.find(User, { email: { $in: inputs.map((i) => i.email) } }); + if (existing_users.length > 0) - throw new BadRequestException( - existing_users.length === 1 - ? this.t.Errors.Email.IsAlreadyUsed(existing_users[0].email) - : this.t.Errors.Email.AreAlreadyUsed(existing_users.map((u) => u.email)), - ); + if (existing_users.length === 1) + throw new i18nBadRequestException('validations.email.invalid.used', { email: existing_users[0].email }); + else + throw new i18nBadRequestException('validations.email.invalid.are_used', { + emails: existing_users.map((u) => u.email).join("', '"), + }); const users: User[] = []; for (const input of inputs) { this.emailsService.validateEmail(input.email); + if (!checkBirthDate(input.birth_date)) - throw new BadRequestException(this.t.Errors.BirthDate.Invalid(input.birth_date)); + throw new i18nBadRequestException('validations.birth_date.invalid.outbound', { date: input.birth_date }); // Generate a random password & hash it const password = generateRandomPassword(12); @@ -279,66 +279,55 @@ export class UsersDataService { } @CreateRequestContext() - async verifyEmail(id: number, token: string): Promise { + async verifyEmail(id: number, token: string): Promise { const user = await this.orm.em.findOne(User, { id }); - if (user.email_verified) throw new BadRequestException(this.t.Errors.Email.AlreadyVerified(User)); + if (user.email_verified) throw new i18nBadRequestException('validations.email.invalid.already_verified'); if (!compareSync(token, user.email_verification)) - throw new UnauthorizedException(this.t.Errors.Email.InvalidVerificationToken()); + throw new i18nUnauthorizedException('validations.token.invalid.format'); if (user.verified === null) user.verified = new Date(); user.email_verified = true; user.email_verification = null; await this.orm.em.persistAndFlush(user); - return { - message: this.t.Success.Email.Verified(user.email), - statusCode: 200, - }; + return new OutputMessageDTO('validations.email.success.verified'); } @CreateRequestContext() - async update(requestUserId: number, inputs: UserPatchDTO[]): Promise { - const users: User[] = []; - - for (const input of inputs) { - const user = await this.orm.em.findOne(User, { id: input.id }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, input.id)); - - if (input.email) await this.updateUserEmail(user.id, input.email); + async update(requestUserId: number, userId: number, input: InputUpdateUserDTO): Promise { + const user = await this.orm.em.findOne(User, { id: userId }); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id: userId }); - if ( - input.hasOwnProperty('birth_date') || - input.hasOwnProperty('first_name') || - input.hasOwnProperty('last_name') - ) { - const currentUser = await this.findOne(requestUserId, false); + if (input.email) await this.updateUserEmail(user.id, input.email); - if (currentUser.id === user.id) - throw new UnauthorizedException(this.t.Errors.User.CannotUpdateBirthDateOrName()); - } - - Object.assign(user, input); + if (input.hasOwnProperty('birth_date') || input.hasOwnProperty('first_name') || input.hasOwnProperty('last_name')) { + const currentUser = await this.findOne(requestUserId, false); - await this.orm.em.persistAndFlush(user); - users.push(user); + if (currentUser.id === user.id) throw new i18nUnauthorizedException('validations.user.cannot_update'); } - return this.sanitize(users); + Object.assign(user, input); + + await this.orm.em.persistAndFlush(user); + return (await this.sanitize([user]))[0]; } @CreateRequestContext() - async delete(id: number): Promise { + async delete(id: number): Promise { const user = await this.orm.em.findOne(User, { id }); await this.orm.em.removeAndFlush(user); - return { message: this.t.Success.Entity.Deleted(User), statusCode: 200 }; + return new OutputMessageDTO('validations.user.success.deleted', { name: user.full_name }); } @CreateRequestContext() - async getUserRoles(id: number, input: { show_expired: boolean; show_revoked: boolean }): Promise { + async getUserRoles( + id: number, + input: { show_expired: boolean; show_revoked: boolean }, + ): Promise { const user = await this.orm.em.findOne(User, { id }, { populate: ['roles'] }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id }); const roles_base = user.roles.getItems(); const roles_data = await this.orm.em.find(RoleExpiration, { user: { $in: [user] } }); @@ -364,16 +353,16 @@ export class UsersDataService { async getUserPermissions( id: number, input: { show_expired: boolean; show_revoked: boolean }, - ): Promise { + ): Promise { const user = await this.orm.em.findOne(User, { id }, { populate: ['permissions'] }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id }); const permissions = user.permissions.getItems(); if (!input.show_expired) permissions.filter((p) => p.expires > new Date()); if (!input.show_revoked) permissions.filter((p) => p.revoked === false); - return permissions.map((p) => p.toObject() as unknown as PermissionGetDTO); + return permissions.map((p) => p.toObject() as unknown as OutputPermissionDTO); } /** diff --git a/src/modules/users/services/users-files.service.ts b/src/modules/users/services/users-files.service.ts index 9f11ad4f..851547fe 100644 --- a/src/modules/users/services/users-files.service.ts +++ b/src/modules/users/services/users-files.service.ts @@ -1,15 +1,15 @@ import { join } from 'path'; import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; -import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { env } from '@env'; -import { MessageResponseDTO } from '@modules/_mixin/dto/message.dto'; -import { FilesService } from '@modules/files/files.service'; -import { TranslateService } from '@modules/translate/translate.service'; +import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; +import { ImagesService } from '@modules/files/images.service'; import { UsersDataService } from './users-data.service'; -import { UserGetBannerDTO, UserGetPictureDTO } from '../dto/get.dto'; +import { OutputUserBannerDTO, OutputUserPictureDTO } from '../dto/output.dto'; import { UserBanner } from '../entities/user-banner.entity'; import { UserPicture } from '../entities/user-picture.entity'; import { User } from '../entities/user.entity'; @@ -17,9 +17,8 @@ import { User } from '../entities/user.entity'; @Injectable() export class UsersFilesService { constructor( - private readonly t: TranslateService, private readonly orm: MikroORM, - private readonly filesService: FilesService, + private readonly imagesService: ImagesService, private readonly dataService: UsersDataService, ) {} @@ -31,26 +30,26 @@ export class UsersFilesService { * @returns {Promise} The updated user */ @CreateRequestContext() - async updatePicture(req_user: User, owner_id: number, file: Express.Multer.File): Promise { + async updatePicture(req_user: User, owner_id: number, file: Express.Multer.File): Promise { const user = await this.orm.em.findOne(User, { id: owner_id }, { populate: ['picture'] }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, owner_id)); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id: owner_id }); if (req_user.id === user.id && user.picture !== null) { const cooldown = env.USERS_PICTURES_DELAY; const now = Date.now(); if ( - !(await this.dataService.hasPermissionOrRoleWithPermission(user.id, false, ['CAN_EDIT_USER'])) && - user.picture.updated.getTime() + cooldown > now + !(await this.dataService.hasPermissionOrRoleWithPermission(req_user.id, false, ['CAN_EDIT_USER'])) && + user.picture.updated.getTime() + cooldown >= now ) { // Throw error if cooldown is not yet passed - throw new UnauthorizedException( - this.t.Errors.User.PictureCooldown(user.picture.updated.getTime() + cooldown - now), - ); + throw new i18nUnauthorizedException('validations.user.picture.cooldown', { + days: cooldown / 1000 / 60 / 60 / 24, + }); } } - const fileInfos = await this.filesService.writeOnDiskAsImage(file, { + const fileInfos = await this.imagesService.writeOnDisk(file.buffer, { directory: join(env.USERS_BASE_PATH, 'pictures'), filename: user.full_name.toLowerCase().replaceAll(' ', '_'), aspect_ratio: '1:1', @@ -58,7 +57,7 @@ export class UsersFilesService { // Remove old file if present if (user.picture) { - this.filesService.deleteFromDisk(user.picture); + this.imagesService.deleteFromDisk(user.picture); user.picture.filename = fileInfos.filename; user.picture.mimetype = fileInfos.mimetype; @@ -75,40 +74,40 @@ export class UsersFilesService { path: fileInfos.filepath, picture_user: user, size: fileInfos.size, - visibility: await this.filesService.getVisibilityGroup(), + visibility: await this.imagesService.getVisibilityGroup(), }); await this.orm.em.persistAndFlush(user); - return user.picture.toObject() as unknown as UserGetPictureDTO; + return user.picture.toObject() as unknown as OutputUserPictureDTO; } @CreateRequestContext() async getPicture(id: number): Promise { const user = await this.orm.em.findOne(User, { id }, { populate: ['picture', 'picture.visibility'] }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); - if (!user.picture) throw new NotFoundException(this.t.Errors.User.NoPicture(id)); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id }); + if (!user.picture) throw new i18nNotFoundException('validations.user.picture.not_found', { name: user.full_name }); return user.picture; } @CreateRequestContext() - async deletePicture(id: number): Promise { + async deletePicture(id: number): Promise { const user = await this.orm.em.findOne(User, { id }, { populate: ['picture'] }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); - if (!user.picture) throw new NotFoundException(this.t.Errors.User.NoPicture(id)); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id }); + if (!user.picture) throw new i18nNotFoundException('validations.user.picture.not_found', { name: user.full_name }); - this.filesService.deleteFromDisk(user.picture); + this.imagesService.deleteFromDisk(user.picture); await this.orm.em.removeAndFlush(user.picture); - return { message: this.t.Success.Entity.Deleted(UserPicture), statusCode: 201 }; + return new OutputMessageDTO('validations.user.success.deleted_picture', { name: user.full_name }); } @CreateRequestContext() - async updateBanner(id: number, file: Express.Multer.File): Promise { + async updateBanner(id: number, file: Express.Multer.File): Promise { const user = await this.orm.em.findOne(User, { id }, { populate: ['banner'] }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id }); - const fileInfos = await this.filesService.writeOnDiskAsImage(file, { + const fileInfos = await this.imagesService.writeOnDisk(file.buffer, { directory: join(env.USERS_BASE_PATH, 'banners'), filename: user.full_name.replaceAll(' ', '_'), aspect_ratio: '16:9', @@ -116,7 +115,7 @@ export class UsersFilesService { // Remove old file if present if (user.banner) { - this.filesService.deleteFromDisk(user.banner); + this.imagesService.deleteFromDisk(user.banner); user.banner.filename = fileInfos.filename; user.banner.mimetype = fileInfos.mimetype; @@ -133,31 +132,31 @@ export class UsersFilesService { path: fileInfos.filepath, banner_user: user, size: fileInfos.size, - visibility: await this.filesService.getVisibilityGroup(), + visibility: await this.imagesService.getVisibilityGroup(), }); await this.orm.em.persistAndFlush(user); - return user.banner.toObject() as unknown as UserGetBannerDTO; + return user.banner.toObject() as unknown as OutputUserBannerDTO; } @CreateRequestContext() async getBanner(id: number): Promise { const user = await this.orm.em.findOne(User, { id }, { populate: ['banner', 'banner.visibility'] }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); - if (!user.banner) throw new NotFoundException(this.t.Errors.User.NoBanner(id)); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id }); + if (!user.banner) throw new i18nNotFoundException('validations.user.banner.not_found', { name: user.full_name }); return user.banner; } @CreateRequestContext() - async deleteBanner(id: number): Promise { + async deleteBanner(id: number): Promise { const user = await this.orm.em.findOne(User, { id }, { populate: ['banner'] }); - if (!user) throw new NotFoundException(this.t.Errors.Id.NotFound(User, id)); - if (!user.banner) throw new NotFoundException(this.t.Errors.User.NoBanner(id)); + if (!user) throw new i18nNotFoundException('validations.user.not_found.id', { id }); + if (!user.banner) throw new i18nNotFoundException('validations.user.banner.not_found', { name: user.full_name }); - this.filesService.deleteFromDisk(user.banner); + this.imagesService.deleteFromDisk(user.banner); await this.orm.em.removeAndFlush(user.banner); - return { message: this.t.Success.Entity.Deleted(UserBanner), statusCode: 201 }; + return new OutputMessageDTO('validations.user.success.deleted_banner', { name: user.full_name }); } } diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index 0e87dedd..0f1e3b57 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -6,7 +6,6 @@ import { AuthService } from '@modules/auth/auth.service'; import { EmailsService } from '@modules/emails/emails.service'; import { FilesService } from '@modules/files/files.service'; import { ImagesService } from '@modules/files/images.service'; -import { TranslateService } from '@modules/translate/translate.service'; import { UserVisibility } from '@modules/users/entities/user-visibility.entity'; import { User } from '@modules/users/entities/user.entity'; @@ -17,16 +16,7 @@ import { UsersFilesService } from './services/users-files.service'; @Module({ imports: [MikroOrmModule.forFeature([User, UserVisibility])], - providers: [ - AuthService, - EmailsService, - FilesService, - ImagesService, - JwtService, - TranslateService, - UsersDataService, - UsersFilesService, - ], + providers: [AuthService, EmailsService, FilesService, ImagesService, JwtService, UsersDataService, UsersFilesService], controllers: [UsersDataController, UsersFilesController], exports: [UsersDataService, UsersFilesService], }) diff --git a/src/utils/dates.ts b/src/utils/dates.ts index fab05a95..19940171 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -1,6 +1,6 @@ /** * Determines if a date is valid for a user's birth date - * (must be at least 13 years old and not in the future) + * (must be at least 13 years old) * @param {string|Date} birth_date The date to check * @returns {boolean} True if the date is valid, false otherwise */ @@ -8,9 +8,6 @@ export function checkBirthDate(birth_date: string | Date): boolean { const date = typeof birth_date === 'string' ? new Date(birth_date) : birth_date; const now = new Date(); - // Check if the date is in the future - if (date > now) return false; - // Check if the user is at least 13 years old const diff = now.getFullYear() - date.getFullYear(); if (diff < 13) return false; diff --git a/src/utils/password.ts b/src/utils/password.ts deleted file mode 100644 index 1be24a69..00000000 --- a/src/utils/password.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { randomInt } from '@exported/global/utils'; - -const SPECIAL_CHARS = '!@#$%^&*()'; -const LOWERCASE_CHARS = 'abcdefghijklmnopqrstuvwxyz'; -const UPPERCASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; -const NUMBERS = '0123456789'; - -const MINIMUM_PASSWORD_LENGTH = 8; - -/** - * Generates a random password of the given length. - * @param {number} length the length of the password to generate - * @returns {string} the generated password - */ -export function generateRandomPassword(length: number = MINIMUM_PASSWORD_LENGTH): string { - if (length < MINIMUM_PASSWORD_LENGTH) length = MINIMUM_PASSWORD_LENGTH; - - const password = [ - SPECIAL_CHARS[randomInt(SPECIAL_CHARS.length - 1)], - LOWERCASE_CHARS[randomInt(LOWERCASE_CHARS.length - 1)], - UPPERCASE_CHARS[randomInt(UPPERCASE_CHARS.length - 1)], - NUMBERS[randomInt(NUMBERS.length - 1)], - ].shuffle(); - - const remainingLength = length - password.length; - - for (let i = 0; i < remainingLength; i++) { - const charSet = SPECIAL_CHARS + LOWERCASE_CHARS + UPPERCASE_CHARS + NUMBERS; - password.push(charSet[randomInt(charSet.length - 1)]); - } - - return password.join(''); -} - -/** - * Check if the password is strong enough - * @param {string} password the password to check - * @returns {boolean} true if the password is strong enough, false otherwise - */ -export function checkPasswordStrength(password: string): boolean { - const regex = new RegExp( - `^(?=.*[${LOWERCASE_CHARS}])(?=.*[${UPPERCASE_CHARS}])(?=.*[${NUMBERS}])(?=.*[${SPECIAL_CHARS}]).{${MINIMUM_PASSWORD_LENGTH},}$`, - ); - return regex.test(password); -} diff --git a/src/utils/validate.ts b/src/utils/validate.ts deleted file mode 100644 index 6046803b..00000000 --- a/src/utils/validate.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; -import { ZodError, ZodTypeAny } from 'zod'; - -/** - * Throws a BadRequestException if the given input does not match the given schema - * @param {ZodTypeAny} schema The schema to validate the input against - * @param {unknown} input The input to validate - * @param {string|undefined} error The error message to throw if the input does not match the schema - */ -export function validate(schema: ZodTypeAny, input: unknown, error?: string): void | never { - try { - schema.parse(input); - } catch (err) { - /* istanbul ignore next-line */ - if (!(err instanceof ZodError)) throw err; - - throw new BadRequestException({ - error: 'Bad Request', - statusCode: 400, - message: error ?? err.format(), - }); - } -} diff --git a/tests/e2e/auth.e2e-spec.ts b/tests/e2e/auth.e2e-spec.ts index ffdb44d1..301fe277 100644 --- a/tests/e2e/auth.e2e-spec.ts +++ b/tests/e2e/auth.e2e-spec.ts @@ -3,14 +3,18 @@ import type { email } from '#types'; import { hashSync } from 'bcrypt'; import request from 'supertest'; -import { UserPostDTO } from '@modules/auth/dto/post.dto'; +import { generateRandomPassword } from '@modules/_mixin/decorators'; +import { OutputCreatedDTO, OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { i18nForbiddenException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; +import { i18nBadRequestException } from '@modules/_mixin/http-errors/bad-request'; +import { InputRegisterUserDTO } from '@modules/auth/dto/input.dto'; import { User } from '@modules/users/entities/user.entity'; -import { generateRandomPassword } from '@utils/password'; -import { orm, server, t } from '..'; +import { orm, server } from '..'; describe('Auth (e2e)', () => { let em: typeof orm.em; + const fakeToken = 'token67891012'; // from tests.seeder.ts beforeAll(() => { em = orm.em.fork(); @@ -19,17 +23,13 @@ describe('Auth (e2e)', () => { describe('(POST) /auth/login', () => { describe('400 : Bad Request', () => { it('when email/password is not provided', async () => { - const response = await request(server).post('/auth/login').send({ password: 'password' }).expect(400); + const response = await request(server).post('/auth/login').send().expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: { - _errors: [], - email: { - _errors: ['Required'], - }, - }, + ...new i18nBadRequestException([ + { key: 'validations.email.invalid.format', args: { property: 'email', value: undefined } }, + { key: 'validations.password.invalid.weak', args: { property: 'password', value: undefined } }, + ]), }); }); }); @@ -38,27 +38,33 @@ describe('Auth (e2e)', () => { it('when password is incorrect', async () => { const response = await request(server) .post('/auth/login') - .send({ email: 'ae.info@utbm.fr', password: '' }) + .send({ email: 'ae.info@utbm.fr', password: generateRandomPassword() }) .expect(401); - expect(response.body).toEqual({ - error: 'Unauthorized', - statusCode: 401, - message: t.Errors.Password.Mismatch(), - }); + expect(response.body).toEqual({ ...new i18nUnauthorizedException('validations.password.invalid.mismatch') }); + }); + }); + + describe('403 : Forbidden', () => { + it('when the user account is not yet verified', async () => { + const response = await request(server) + .post('/auth/login') + .send({ email: 'unverified@email.com', password: 'root' }) + .expect(403); + + expect(response.body).toEqual({ ...new i18nForbiddenException('validations.user.unverified') }); }); }); describe('404 : Not Found', () => { it('when user is not found', async () => { - const email: email = 'doesnotexist@utbm.fr'; - const response = await request(server).post('/auth/login').send({ email, password: '' }).expect(404); + const email: email = 'doesnotexist@example.fr'; + const response = await request(server) + .post('/auth/login') + .send({ email, password: generateRandomPassword() }) + .expect(404); - expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Email.NotFound(User, email), - }); + expect(response.body).toEqual({ ...new i18nNotFoundException('validations.user.not_found.email', { email }) }); }); }); @@ -78,7 +84,7 @@ describe('Auth (e2e)', () => { }); describe('(POST) /auth/register', () => { - const user: UserPostDTO = { + const user: InputRegisterUserDTO = { first_name: 'John', last_name: 'Doe', email: 'johndoe@domain.com', @@ -96,9 +102,7 @@ describe('Auth (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.BirthDate.Invalid(tomorrow), + ...new i18nBadRequestException('validations.birth_date.invalid.outbound', { date: tomorrow.toISOString() }), }); }); @@ -111,9 +115,7 @@ describe('Auth (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.BirthDate.Invalid(birth_date), + ...new i18nBadRequestException('validations.birth_date.invalid.outbound', { date: birth_date.toISOString() }), }); }); @@ -125,9 +127,7 @@ describe('Auth (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Password.Weak(), + ...new i18nBadRequestException('validations.password.invalid.weak', { property: 'password', value: 'short' }), }); }); @@ -139,14 +139,7 @@ describe('Auth (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: { - _errors: [], - email: { - _errors: ['Invalid email'], - }, - }, + ...new i18nBadRequestException('validations.email.invalid.format', { property: 'email', value: 'invalid' }), }); }); @@ -158,9 +151,7 @@ describe('Auth (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Email.Blacklisted(email as unknown as email), + ...new i18nBadRequestException('validations.email.invalid.blacklisted', { property: 'email', value: email }), }); }); @@ -171,11 +162,7 @@ describe('Auth (e2e)', () => { .send({ ...user, email }) .expect(400); - expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Email.IsAlreadyUsed(email), - }); + expect(response.body).toEqual({ ...new i18nBadRequestException('validations.email.invalid.used', { email }) }); }); it('when one of required fields is not provided', async () => { @@ -185,29 +172,10 @@ describe('Auth (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: { - _errors: [], - first_name: { - _errors: ['Required'], - }, - }, - }); - }); - - it('when one unexpected field is given', async () => { - const response = await request(server) - .post('/auth/register') - .send({ ...user, never_gonna: 'give_you_up' }) - .expect(400); - - expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: { - _errors: ["Unrecognized key(s) in object: 'never_gonna'"], - }, + ...new i18nBadRequestException('validations.string.invalid.format', { + property: 'first_name', + value: undefined, + }), }); }); }); @@ -220,8 +188,9 @@ describe('Auth (e2e)', () => { .expect(201); expect(response.body).toEqual({ - message: t.Success.User.Registered(), - statusCode: 201, + ...new OutputCreatedDTO('validations.user.success.registered', { + name: user.first_name + ' ' + user.last_name, + }), }); }); }); @@ -230,68 +199,45 @@ describe('Auth (e2e)', () => { describe('(GET) /auth/confirm/:user_id/:token', () => { // Defined in the seeder class (unverified user) const user_id = 2; - const token = 'token67891012'; describe('400 : Bad Request', () => { it('when the "user_id" is not a number', async () => { const fakeId = 'invalid'; - const response = await request(server).get(`/auth/confirm/${fakeId}/${token}`).expect(400); - - expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, fakeId), - }); - }); - - it('when the "token" is invalid', async () => { - const token = ' '; - const response = await request(server).get(`/auth/confirm/1/${token}/`).expect(400); + const response = await request(server).get(`/auth/confirm/${fakeId}/${fakeToken}`).expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.JWT.Invalid(), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'user_id', value: fakeId }), }); }); it("when the user's email is already verified", async () => { - const response = await request(server).get(`/auth/confirm/1/anything1012`).expect(400); + const response = await request(server).get(`/auth/confirm/1/${fakeToken}`).expect(400); - expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Email.AlreadyVerified(User), - }); + expect(response.body).toEqual({ ...new i18nBadRequestException('validations.email.invalid.already_verified') }); }); }); describe('401 : Unauthorized', () => { - it('when the token is invalid', async () => { + it('when the "token" is invalid', async () => { const response = await request(server).get(`/auth/confirm/${user_id}/invalid_token`).expect(401); expect(response.body).toEqual({ - error: 'Unauthorized', - statusCode: 401, - message: t.Errors.Email.InvalidVerificationToken(), + ...new i18nUnauthorizedException('validations.token.invalid.format'), }); }); }); describe('200 : Ok', () => { it('when user is verified', async () => { - const response = await request(server).get(`/auth/confirm/${user_id}/${token}`).expect(200); + const response = await request(server).get(`/auth/confirm/${user_id}/${fakeToken}`).expect(200); - expect(response.body).toEqual({ - message: t.Success.Email.Verified('unverified@email.com'), - statusCode: 200, - }); + expect(response.body).toEqual({ ...new OutputMessageDTO('validations.email.success.verified') }); // Reset user email_verified to false (for other tests) const user = await em.findOne(User, { id: user_id }); user.email_verified = false; - user.email_verification = hashSync(token, 10); + user.email_verification = hashSync(fakeToken, 10); user.verified = null; await em.persistAndFlush(user); diff --git a/tests/e2e/logs.e2e-spec.ts b/tests/e2e/logs.e2e-spec.ts index d4f74b29..28f07e9f 100644 --- a/tests/e2e/logs.e2e-spec.ts +++ b/tests/e2e/logs.e2e-spec.ts @@ -1,10 +1,10 @@ import request from 'supertest'; -import { TokenDTO } from '@modules/auth/dto/get.dto'; -import { Log } from '@modules/logs/entities/log.entity'; -import { User } from '@modules/users/entities/user.entity'; +import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { i18nBadRequestException } from '@modules/_mixin/http-errors'; +import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; -import { server, t } from '..'; +import { server } from '..'; describe('Logs (e2e)', () => { let tokenUnauthorized: string; @@ -17,7 +17,7 @@ describe('Logs (e2e)', () => { let userIdLogModerator: number; beforeAll(async () => { - type res = Omit & { body: TokenDTO }; + type res = Omit & { body: OutputTokenDTO }; const responseA: res = await request(server).post('/auth/login').send({ email: 'promos@email.com', @@ -55,9 +55,7 @@ describe('Logs (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, fakeId), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'id', value: fakeId }), }); }); }); @@ -163,9 +161,7 @@ describe('Logs (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, fakeId), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'id', value: fakeId }), }); }); }); @@ -204,8 +200,7 @@ describe('Logs (e2e)', () => { .expect(200); expect(response.body).toEqual({ - statusCode: 200, - message: t.Success.Entity.Deleted(Log), + ...new OutputMessageDTO('validations.logs.success.deleted'), }); }); }); diff --git a/tests/e2e/permissions.e2e-spec.ts b/tests/e2e/permissions.e2e-spec.ts index 1da77a31..ed099e9d 100644 --- a/tests/e2e/permissions.e2e-spec.ts +++ b/tests/e2e/permissions.e2e-spec.ts @@ -1,10 +1,11 @@ import request from 'supertest'; -import { TokenDTO } from '@modules/auth/dto/get.dto'; +import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { Permission } from '@modules/permissions/entities/permission.entity'; -import { User } from '@modules/users/entities/user.entity'; -import { server, t, orm } from '..'; +import { server, orm } from '..'; describe('Permissions (e2e)', () => { let tokenUnauthorized: string; @@ -13,7 +14,7 @@ describe('Permissions (e2e)', () => { beforeAll(async () => { em = orm.em.fork(); - type res = Omit & { body: TokenDTO }; + type res = Omit & { body: OutputTokenDTO }; const resA: res = await request(server).post('/auth/login').send({ email: 'unauthorized@email.com', @@ -40,9 +41,11 @@ describe('Permissions (e2e)', () => { .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: t.Errors.Permission.Invalid('INVALID'), + ...new i18nBadRequestException('validations.permission.invalid.format', { + property: 'permission', + value: 'INVALID', + permissions: PERMISSIONS_NAMES.join("', '"), + }), }); }); @@ -54,14 +57,7 @@ describe('Permissions (e2e)', () => { .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: { - _errors: [], - expires: { - _errors: ['Required'], - }, - }, + ...new i18nBadRequestException('validations.date.invalid.format', { property: 'expires', value: undefined }), }); }); @@ -73,9 +69,10 @@ describe('Permissions (e2e)', () => { .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: t.Errors.Permission.AlreadyOnUser('ROOT', 'root root'), + ...new i18nBadRequestException('validations.permission.invalid.already_on', { + permission: 'ROOT', + name: 'root root', + }), }); }); }); @@ -115,9 +112,7 @@ describe('Permissions (e2e)', () => { .expect(404); expect(response.body).toEqual({ - statusCode: 404, - error: 'Not Found', - message: t.Errors.Id.NotFound(User, 999999), + ...new i18nNotFoundException('validations.user.not_found.id', { id: 999999 }), }); }); }); @@ -185,9 +180,7 @@ describe('Permissions (e2e)', () => { .expect(404); expect(response.body).toEqual({ - statusCode: 404, - error: 'Not Found', - message: t.Errors.Id.NotFound(User, 999999), + ...new i18nNotFoundException('validations.user.not_found.id', { id: 999999 }), }); }); @@ -199,9 +192,7 @@ describe('Permissions (e2e)', () => { .expect(404); expect(response.body).toEqual({ - statusCode: 404, - error: 'Not Found', - message: t.Errors.Permission.NotFoundOnUser('ROOT', 'root root'), + ...new i18nNotFoundException('validations.permission.not_found', { id: 999999, name: 'root root' }), }); }); }); @@ -227,7 +218,7 @@ describe('Permissions (e2e)', () => { }); }); - describe('(GET) /permissions/:user_id', () => { + describe('(GET) /permissions/:id', () => { describe('400 : Bad Request', () => { it('when the user ID is not valid', async () => { const response = await request(server) @@ -236,9 +227,7 @@ describe('Permissions (e2e)', () => { .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: t.Errors.Id.Invalid(User, 'abc'), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'id', value: 'abc' }), }); }); }); @@ -277,9 +266,7 @@ describe('Permissions (e2e)', () => { .expect(404); expect(response.body).toEqual({ - statusCode: 404, - error: 'Not Found', - message: t.Errors.Id.NotFound(User, 999999), + ...new i18nNotFoundException('validations.user.not_found.id', { id: 999999 }), }); }); }); diff --git a/tests/e2e/promotions.e2e-spec.ts b/tests/e2e/promotions.e2e-spec.ts index b87f09fe..5a9ee558 100644 --- a/tests/e2e/promotions.e2e-spec.ts +++ b/tests/e2e/promotions.e2e-spec.ts @@ -4,11 +4,13 @@ import { join } from 'path'; import request from 'supertest'; import { env } from '@env'; -import { TokenDTO } from '@modules/auth/dto/get.dto'; +import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { PromotionPicture } from '@modules/promotions/entities/promotion-picture.entity'; import { Promotion } from '@modules/promotions/entities/promotion.entity'; -import { server, t, orm } from '..'; +import { server, orm } from '..'; describe('Promotions (e2e)', () => { let tokenUnauthorized: string; @@ -21,7 +23,7 @@ describe('Promotions (e2e)', () => { beforeAll(async () => { em = orm.em.fork(); - type res = Omit & { body: TokenDTO }; + type res = Omit & { body: OutputTokenDTO }; const resA: res = await request(server).post('/auth/login').send({ email: 'unauthorized@email.com', @@ -186,9 +188,7 @@ describe('Promotions (e2e)', () => { .set('Authorization', `Bearer ${tokenPromotionModerator}`); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: t.Errors.Id.Invalid(Promotion, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'number', value: 'invalid' }), }); }); }); @@ -225,9 +225,7 @@ describe('Promotions (e2e)', () => { .set('Authorization', `Bearer ${tokenPromotionModerator}`); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Id.NotFound('Promotion', 999999), + ...new i18nNotFoundException('validations.promotion.invalid.not_found', { number: 999999 }), }); }); }); @@ -257,9 +255,7 @@ describe('Promotions (e2e)', () => { .set('Authorization', `Bearer ${tokenPromotionModerator}`); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: t.Errors.Id.Invalid(Promotion, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'number', value: 'invalid' }), }); }); }); @@ -296,9 +292,7 @@ describe('Promotions (e2e)', () => { .set('Authorization', `Bearer ${tokenPromotionModerator}`); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Id.NotFound(Promotion, 999999), + ...new i18nNotFoundException('validations.promotion.invalid.not_found', { number: 999999 }), }); }); }); @@ -331,9 +325,7 @@ describe('Promotions (e2e)', () => { .set('Authorization', `Bearer ${tokenPromotionModerator}`); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: t.Errors.Id.Invalid(Promotion, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'number', value: 'invalid' }), }); }); }); @@ -370,9 +362,7 @@ describe('Promotions (e2e)', () => { .set('Authorization', `Bearer ${tokenPromotionModerator}`); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Id.NotFound(Promotion, 999999), + ...new i18nNotFoundException('validations.promotion.invalid.not_found', { number: 999999 }), }); }); @@ -382,9 +372,7 @@ describe('Promotions (e2e)', () => { .set('Authorization', `Bearer ${tokenPromotionModerator}`); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Promotion.LogoNotFound(21), + ...new i18nNotFoundException('validations.promotion.invalid.no_logo', { number: 21 }), }); }); }); @@ -431,9 +419,7 @@ describe('Promotions (e2e)', () => { .set('Authorization', `Bearer ${tokenPromotionModerator}`); expect(response.body).toEqual({ - error: 'Bad Request', - message: t.Errors.File.NotProvided(), - statusCode: 400, + ...new i18nBadRequestException('validations.file.invalid.not_provided'), }); }); @@ -444,9 +430,7 @@ describe('Promotions (e2e)', () => { .attach('file', fileNotAnImage, 'file.txt'); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.File.InvalidMimeType(['image/*']), + ...new i18nBadRequestException('validations.file.invalid.unauthorized_mime_type', { mime_types: 'image/*' }), }); }); @@ -457,9 +441,7 @@ describe('Promotions (e2e)', () => { .attach('file', filePictureNotSquare, 'file.png'); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Image.InvalidAspectRatio('1:1'), + ...new i18nBadRequestException('validations.image.invalid.aspect_ratio', { aspect_ratio: '1:1' }), }); }); @@ -470,9 +452,7 @@ describe('Promotions (e2e)', () => { .attach('file', filePictureSquare, 'file.png'); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: t.Errors.Id.Invalid(Promotion, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'number', value: 'invalid' }), }); }); }); @@ -510,9 +490,7 @@ describe('Promotions (e2e)', () => { .attach('file', filePictureSquare, 'file.png'); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Id.NotFound(Promotion, 999999), + ...new i18nNotFoundException('validations.promotion.invalid.not_found', { number: 999999 }), }); }); }); @@ -582,9 +560,7 @@ describe('Promotions (e2e)', () => { .set('Authorization', `Bearer ${tokenPromotionModerator}`); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: t.Errors.Id.Invalid(Promotion, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'number', value: 'invalid' }), }); }); }); @@ -621,9 +597,7 @@ describe('Promotions (e2e)', () => { .set('Authorization', `Bearer ${tokenPromotionModerator}`); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Id.NotFound(Promotion, 999999), + ...new i18nNotFoundException('validations.promotion.invalid.not_found', { number: 999999 }), }); }); @@ -633,9 +607,7 @@ describe('Promotions (e2e)', () => { .set('Authorization', `Bearer ${tokenPromotionModerator}`); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Promotion.LogoNotFound(20), + ...new i18nNotFoundException('validations.promotion.invalid.no_logo', { number: 20 }), }); }); }); @@ -660,8 +632,7 @@ describe('Promotions (e2e)', () => { .expect(200); expect(response.body).toEqual({ - message: t.Success.Entity.Deleted(PromotionPicture), - statusCode: 200, + ...new OutputMessageDTO('validations.promotion.success.deleted_logo', { number: 21 }), }); // expect the file to be deleted from disk diff --git a/tests/e2e/roles.e2e-spec.ts b/tests/e2e/roles.e2e-spec.ts index ac8a22a5..5c3f5c5d 100644 --- a/tests/e2e/roles.e2e-spec.ts +++ b/tests/e2e/roles.e2e-spec.ts @@ -1,9 +1,11 @@ import request from 'supertest'; +import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { InputUpdateRoleUserDTO } from '@modules/roles/dto/input.dto'; import { Role } from '@modules/roles/entities/role.entity'; -import { User } from '@modules/users/entities/user.entity'; -import { server, t, orm } from '..'; +import { server, orm } from '..'; describe('Roles (e2e)', () => { let tokenUnauthorized: string; @@ -95,14 +97,14 @@ describe('Roles (e2e)', () => { .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: { - _errors: [], - name: { - _errors: ['Required'], + ...new i18nBadRequestException([ + { key: 'validations.string.invalid.uppercase', args: { property: 'name', value: undefined } }, + { key: 'validations.string.invalid.format', args: { property: 'name', value: undefined } }, + { + key: 'validations.permission.invalid.format', + args: { property: 'permissions', value: 'test', permissions: PERMISSIONS_NAMES.join("', '") }, }, - }, + ]), }); }); @@ -117,9 +119,11 @@ describe('Roles (e2e)', () => { .expect(400); expect(response.body).toEqual({ - statusCode: 400, - message: t.Errors.Permission.Invalid('TEST'), - error: 'Bad Request', + ...new i18nBadRequestException('validations.permission.invalid.format', { + property: 'permissions', + value: 'TEST', + permissions: PERMISSIONS_NAMES.join("', '"), + }), }); }); @@ -134,9 +138,7 @@ describe('Roles (e2e)', () => { .expect(400); expect(response.body).toEqual({ - statusCode: 400, - message: t.Errors.Role.NameAlreadyUsed('PERMISSIONS_MODERATOR'), - error: 'Bad Request', + ...new i18nBadRequestException('validations.role.invalid.already_exist', { name: 'PERMISSIONS_MODERATOR' }), }); }); }); @@ -204,17 +206,14 @@ describe('Roles (e2e)', () => { .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: { - _errors: [], - id: { - _errors: ['Required'], - }, - name: { - _errors: ['Invalid input'], + ...new i18nBadRequestException([ + { key: 'validations.id.invalid.format', args: { property: 'id', value: undefined } }, + { key: 'validations.string.invalid.uppercase', args: { property: 'name', value: 'test' } }, + { + key: 'validations.permission.invalid.format', + args: { property: 'permissions', value: 'test', permissions: PERMISSIONS_NAMES.join("', '") }, }, - }, + ]), }); }); }); @@ -253,14 +252,12 @@ describe('Roles (e2e)', () => { .send({ id: 9999, name: 'TEST', - permissions: ['TEST'], + permissions: ['ROOT'], }) .expect(404); expect(response.body).toEqual({ - statusCode: 404, - message: t.Errors.Id.NotFound(Role, 9999), - error: 'Not Found', + ...new i18nNotFoundException('validations.role.not_found', { id: 9999 }), }); }); }); @@ -300,9 +297,7 @@ describe('Roles (e2e)', () => { .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: t.Errors.Id.Invalid(Role, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'id', value: 'invalid' }), }); }); }); @@ -341,9 +336,7 @@ describe('Roles (e2e)', () => { .expect(404); expect(response.body).toEqual({ - statusCode: 404, - message: t.Errors.Id.NotFound(Role, 9999), - error: 'Not Found', + ...new i18nNotFoundException('validations.role.not_found', { id: 9999 }), }); }); }); @@ -382,9 +375,7 @@ describe('Roles (e2e)', () => { .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: t.Errors.Id.Invalid(Role, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'id', value: 'invalid' }), }); }); }); @@ -423,9 +414,7 @@ describe('Roles (e2e)', () => { .expect(404); expect(response.body).toEqual({ - statusCode: 404, - message: t.Errors.Id.NotFound(Role, 9999), - error: 'Not Found', + ...new i18nNotFoundException('validations.role.not_found', { id: 9999 }), }); }); }); @@ -458,53 +447,52 @@ describe('Roles (e2e)', () => { const response = await request(server) .post('/roles/invalid/users') .set('Authorization', `Bearer ${tokenRolesModerator}`) - .send([ - { - id: 1, - expires: new Date('9021-01-01').toISOString(), - }, - { - id: 2, - expires: new Date('9022-01-01').toISOString(), - }, - { - id: 3, - expires: new Date('9023-01-01').toISOString(), - }, - ]) + .send({ + users: [ + { + id: 1, + expires: new Date('9021-01-01').toISOString(), + }, + { + id: 2, + expires: new Date('9022-01-01').toISOString(), + }, + { + id: 3, + expires: new Date('9023-01-01').toISOString(), + }, + ], + }) .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: t.Errors.Id.Invalid(Role, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'id', value: 'invalid' }), }); }); - it('when one of the user id is invalid', async () => { + it('when one of the body is invalid', async () => { const response = await request(server) .post('/roles/1/users') .set('Authorization', `Bearer ${tokenRolesModerator}`) - .send([ - { - id: 'invalid', - expires: new Date('9021-01-01').toISOString(), - }, - ]) + .send({ + users: [ + { + id: 'invalid', + expires: new Date('9021-01-01').toISOString(), + }, + 'test', + ], + }) .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: { - _errors: [], - 0: { - _errors: [], - id: { - _errors: ['Expected number, received string'], - }, + ...new i18nBadRequestException([ + { key: 'validations.id.invalid.format', args: { property: 'users.0.id', value: 'invalid' } }, + { + key: 'validations.array.invalid.format', + args: { property: 'users.1', value: 'test', type: InputUpdateRoleUserDTO.name }, }, - }, + ]), }); }); @@ -512,15 +500,11 @@ describe('Roles (e2e)', () => { const response = await request(server) .post('/roles/1/users') .set('Authorization', `Bearer ${tokenRolesModerator}`) - .send([]) + .send({ users: [] }) .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: { - _errors: ['Array must contain at least 1 element(s)'], - }, + ...new i18nBadRequestException('validations.array.invalid.not_empty', { property: 'users' }), }); }); }); @@ -556,13 +540,11 @@ describe('Roles (e2e)', () => { const response = await request(server) .post('/roles/9999/users') .set('Authorization', `Bearer ${tokenRolesModerator}`) - .send([{ id: 1, expires: new Date('9021-01-01').toISOString() }]) + .send({ users: [{ id: 1, expires: new Date('9021-01-01').toISOString() }] }) .expect(404); expect(response.body).toEqual({ - statusCode: 404, - message: t.Errors.Id.NotFound(Role, 9999), - error: 'Not Found', + ...new i18nNotFoundException('validations.role.not_found', { id: 9999 }), }); }); @@ -570,13 +552,11 @@ describe('Roles (e2e)', () => { const response = await request(server) .post('/roles/1/users') .set('Authorization', `Bearer ${tokenRolesModerator}`) - .send([{ id: 9999, expires: new Date('9021-01-01').toISOString() }]) + .send({ users: [{ id: 9999, expires: new Date('9021-01-01').toISOString() }] }) .expect(404); expect(response.body).toEqual({ - statusCode: 404, - message: t.Errors.Id.NotFound(User, 9999), - error: 'Not Found', + ...new i18nNotFoundException('validations.users.not_found.ids', { ids: '9999' }), }); }); }); @@ -587,10 +567,12 @@ describe('Roles (e2e)', () => { const response = await request(server) .post(`/roles/${role_id}/users`) .set('Authorization', `Bearer ${tokenRolesModerator}`) - .send([ - { id: 1, expires: new Date('9021-01-01').toISOString() }, - { id: 2, expires: new Date('9022-01-01').toISOString() }, - ]) + .send({ + users: [ + { id: 1, expires: new Date('9021-01-01').toISOString() }, + { id: 2, expires: new Date('9022-01-01').toISOString() }, + ], + }) .expect(201); const body = response.body as Array; @@ -628,13 +610,11 @@ describe('Roles (e2e)', () => { const response = await request(server) .delete('/roles/invalid/users') .set('Authorization', `Bearer ${tokenRolesModerator}`) - .send([1, 2, 3]) + .send({ users: [1, 2, 3] }) .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: t.Errors.Id.Invalid(Role, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { property: 'id', value: 'invalid' }), }); }); @@ -642,18 +622,14 @@ describe('Roles (e2e)', () => { const response = await request(server) .delete('/roles/1/users') .set('Authorization', `Bearer ${tokenRolesModerator}`) - .send(['invalid']) + .send({ users: ['invalid'] }) .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: { - 0: { - _errors: ['Expected number, received string'], - }, - _errors: [], - }, + ...new i18nBadRequestException('validations.id.invalid.format', { + property: 'users', + value: 'invalid', + }), }); }); @@ -661,15 +637,11 @@ describe('Roles (e2e)', () => { const response = await request(server) .delete('/roles/1/users') .set('Authorization', `Bearer ${tokenRolesModerator}`) - .send([]) + .send({ users: [] }) .expect(400); expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: { - _errors: ['Array must contain at least 1 element(s)'], - }, + ...new i18nBadRequestException('validations.array.invalid.not_empty', { property: 'users' }), }); }); }); @@ -705,13 +677,11 @@ describe('Roles (e2e)', () => { const response = await request(server) .delete('/roles/9999/users') .set('Authorization', `Bearer ${tokenRolesModerator}`) - .send([1, 2, 3]) + .send({ users: [1, 2, 3] }) .expect(404); expect(response.body).toEqual({ - statusCode: 404, - message: t.Errors.Id.NotFound(Role, 9999), - error: 'Not Found', + ...new i18nNotFoundException('validations.role.not_found', { id: 9999 }), }); }); @@ -719,13 +689,11 @@ describe('Roles (e2e)', () => { const response = await request(server) .delete('/roles/1/users') .set('Authorization', `Bearer ${tokenRolesModerator}`) - .send([9999]) + .send({ users: [9999] }) .expect(404); expect(response.body).toEqual({ - statusCode: 404, - message: t.Errors.Id.NotFound(User, 9999), - error: 'Not Found', + ...new i18nNotFoundException('validations.users.not_found.ids', { ids: 9999 }), }); }); }); @@ -756,7 +724,7 @@ describe('Roles (e2e)', () => { const response2 = await request(server) .delete(`/roles/${role_id}/users`) .set('Authorization', `Bearer ${tokenRolesModerator}`) - .send([2]) + .send({ users: [2] }) .expect(200); const body = response2.body as Array; diff --git a/tests/e2e/users/users-data.e2e-spec.ts b/tests/e2e/users/users-data.e2e-spec.ts index e4077416..830262e4 100644 --- a/tests/e2e/users/users-data.e2e-spec.ts +++ b/tests/e2e/users/users-data.e2e-spec.ts @@ -2,10 +2,12 @@ import type { email } from '#types'; import request from 'supertest'; -import { TokenDTO } from '@modules/auth/dto/get.dto'; +import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; +import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { User } from '@modules/users/entities/user.entity'; -import { orm, t, server } from '../..'; +import { orm, server } from '../..'; describe('Users Data (e2e)', () => { let tokenUnauthorized: string; @@ -15,7 +17,7 @@ describe('Users Data (e2e)', () => { let em: typeof orm.em; const fakeUserEmail: email = 'john.doe@example.fr'; - type res = Omit & { body: TokenDTO }; + type res = Omit & { body: OutputTokenDTO }; beforeAll(async () => { em = orm.em.fork(); @@ -55,27 +57,22 @@ describe('Users Data (e2e)', () => { const response = await request(server) .post('/users') .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - first_name: 'John', - last_name: 'Doe', - birth_date: new Date('1990-01-01'), - }, - ]) + .send({ + users: [ + { + first_name: 'John', + last_name: 'Doe', + birth_date: new Date('1990-01-01'), + }, + ], + }) .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: { - 0: { - _errors: [], - email: { - _errors: ['Required'], - }, - }, - _errors: [], - }, + ...new i18nBadRequestException('validations.email.invalid.format', { + property: 'users.0.email', + value: undefined, + }), }); }); @@ -83,20 +80,20 @@ describe('Users Data (e2e)', () => { const response = await request(server) .post('/users') .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - email: 'ae.info@utbm.fr', - first_name: 'John', - last_name: 'Doe', - birth_date: new Date('1990-01-01'), - }, - ]) + .send({ + users: [ + { + email: 'ae.info@utbm.fr', + first_name: 'John', + last_name: 'Doe', + birth_date: new Date('1990-01-01'), + }, + ], + }) .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Email.IsAlreadyUsed('ae.info@utbm.fr'), + ...new i18nBadRequestException('validations.email.invalid.used', { email: 'ae.info@utbm.fr' }), }); }); @@ -104,26 +101,28 @@ describe('Users Data (e2e)', () => { const response = await request(server) .post('/users') .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - email: 'ae.info@utbm.fr', - first_name: 'John', - last_name: 'Doe', - birth_date: new Date('1990-01-01'), - }, - { - email: 'unverified@email.com', - first_name: 'John', - last_name: 'Doe', - birth_date: new Date('1990-01-01'), - }, - ]) + .send({ + users: [ + { + email: 'ae.info@utbm.fr', + first_name: 'John', + last_name: 'Doe', + birth_date: new Date('1990-01-01'), + }, + { + email: 'unverified@email.com', + first_name: 'John', + last_name: 'Doe', + birth_date: new Date('1990-01-01'), + }, + ], + }) .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Email.AreAlreadyUsed(['ae.info@utbm.fr', 'unverified@email.com']), + ...new i18nBadRequestException('validations.email.invalid.are_used', { + emails: ['unverified@email.com', 'ae.info@utbm.fr'].join("', '"), + }), }); }); @@ -131,20 +130,23 @@ describe('Users Data (e2e)', () => { const response = await request(server) .post('/users') .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - email: 'any@utbm.fr', - first_name: 'John', - last_name: 'Doe', - birth_date: new Date('1990-01-01'), - }, - ]) + .send({ + users: [ + { + email: 'any@utbm.fr', + first_name: 'John', + last_name: 'Doe', + birth_date: new Date('1990-01-01'), + }, + ], + }) .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Email.Blacklisted('any@utbm.fr'), + ...new i18nBadRequestException('validations.email.invalid.blacklisted', { + property: 'users.0.email', + value: 'any@utbm.fr', + }), }); }); @@ -154,20 +156,20 @@ describe('Users Data (e2e)', () => { const response = await request(server) .post('/users') .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - email: 'example123@domain.com', - first_name: 'John', - last_name: 'Doe', - birth_date: date, - }, - ]) + .send({ + users: [ + { + email: 'example123@domain.com', + first_name: 'John', + last_name: 'Doe', + birth_date: date, + }, + ], + }) .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.BirthDate.Invalid(date), + ...new i18nBadRequestException('validations.birth_date.invalid.outbound', { date: date.toISOString() }), }); }); }); @@ -176,14 +178,16 @@ describe('Users Data (e2e)', () => { it('when the user is not authenticated', async () => { const response = await request(server) .post('/users') - .send([ - { - email: 'any@example.com', - first_name: 'John', - last_name: 'Doe', - birth_date: new Date('1990-01-01'), - }, - ]) + .send({ + users: [ + { + email: 'any@example.com', + first_name: 'John', + last_name: 'Doe', + birth_date: new Date('1990-01-01'), + }, + ], + }) .expect(401); expect(response.body).toEqual({ @@ -198,14 +202,16 @@ describe('Users Data (e2e)', () => { const response = await request(server) .post('/users') .set('Authorization', `Bearer ${tokenUnauthorized}`) - .send([ - { - email: 'any@example.com', - first_name: 'John', - last_name: 'Doe', - birth_date: new Date('1990-01-01'), - }, - ]) + .send({ + users: [ + { + email: 'any@example.com', + first_name: 'John', + last_name: 'Doe', + birth_date: new Date('1990-01-01'), + }, + ], + }) .expect(403); expect(response.body).toEqual({ @@ -221,14 +227,16 @@ describe('Users Data (e2e)', () => { const response = await request(server) .post('/users') .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - email: fakeUserEmail, - birth_date: new Date('2001-01-01'), - first_name: 'John', - last_name: 'Doe', - }, - ]) + .send({ + users: [ + { + email: fakeUserEmail, + birth_date: new Date('2001-01-01'), + first_name: 'John', + last_name: 'Doe', + }, + ], + }) .expect(201); expect(response.body).toEqual([ @@ -248,52 +256,33 @@ describe('Users Data (e2e)', () => { describe('400 : Bad Request', () => { it('when the user id is not a number', async () => { const response = await request(server) - .patch('/users') + .patch('/users/abc') .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - id: 'abc', - }, - ]) .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'abc'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'abc', property: 'id' }), }); }); it('when the user email is already used', async () => { const response = await request(server) - .patch('/users') + .patch('/users/1') .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - id: 1, - email: 'unverified@email.com', - }, - ]) + .send({ + email: 'unverified@email.com', + }) .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Email.IsAlreadyUsed('unverified@email.com'), + ...new i18nBadRequestException('validations.email.invalid.used', { email: 'unverified@email.com' }), }); }); }); describe('401 : Unauthorized', () => { it('when the user is not authenticated', async () => { - const response = await request(server) - .patch('/users') - .send([ - { - id: 1, - }, - ]) - .expect(401); + const response = await request(server).patch('/users/1').expect(401); expect(response.body).toEqual({ message: 'Unauthorized', @@ -303,58 +292,43 @@ describe('Users Data (e2e)', () => { it('when the user try to change its own birth date', async () => { const response = await request(server) - .patch('/users') + .patch('/users/1') .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - id: 1, - birth_date: new Date('2001-01-01'), - }, - ]) + .send({ + birth_date: new Date('2001-01-01'), + }) .expect(401); expect(response.body).toEqual({ - error: 'Unauthorized', - statusCode: 401, - message: t.Errors.User.CannotUpdateBirthDateOrName(), + ...new i18nUnauthorizedException('validations.user.cannot_update'), }); }); it('when the user try to change its own first name', async () => { const response = await request(server) - .patch('/users') + .patch('/users/1') .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - id: 1, - first_name: 'John', - }, - ]) + .send({ + first_name: 'John', + }) .expect(401); expect(response.body).toEqual({ - error: 'Unauthorized', - statusCode: 401, - message: t.Errors.User.CannotUpdateBirthDateOrName(), + ...new i18nUnauthorizedException('validations.user.cannot_update'), }); }); it('when the user try to change its own last name', async () => { const response = await request(server) - .patch('/users') + .patch('/users/1') .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - id: 1, - last_name: 'Smith', - }, - ]) + .send({ + last_name: 'Smith', + }) .expect(401); expect(response.body).toEqual({ - error: 'Unauthorized', - statusCode: 401, - message: t.Errors.User.CannotUpdateBirthDateOrName(), + ...new i18nUnauthorizedException('validations.user.cannot_update'), }); }); }); @@ -362,13 +336,8 @@ describe('Users Data (e2e)', () => { describe('403 : Forbidden', () => { it('when the user is not authorized', async () => { const response = await request(server) - .patch('/users') + .patch('/users/1') .set('Authorization', `Bearer ${tokenUnauthorized}`) - .send([ - { - id: 1, - }, - ]) .expect(403); expect(response.body).toEqual({ @@ -382,19 +351,13 @@ describe('Users Data (e2e)', () => { describe('404 : Not Found', () => { it('when the user does not exist', async () => { const response = await request(server) - .patch('/users') + .patch('/users/9999') .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - id: 9999, - }, - ]) + .send({}) .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - message: t.Errors.Id.NotFound(User, 9999), - statusCode: 404, + ...new i18nNotFoundException('validations.user.not_found.id', { id: 9999 }), }); }); }); @@ -406,33 +369,28 @@ describe('Users Data (e2e)', () => { // -> we are updating a user that is not the authenticated one => expect 200 const response = await request(server) - .patch('/users') + .patch(`/users/${user.id}`) .set('Authorization', `Bearer ${tokenRoot}`) - .send([ - { - id: user.id, - birth_date: new Date('1990-01-01').toISOString(), - email: 'john.doe@example.fr', - }, - ]) + .send({ + birth_date: new Date('1990-01-01').toISOString(), + email: 'john.doe@example.fr', + }) .expect(200); - expect(response.body).toEqual([ - { - id: expect.any(Number), - created: expect.any(String), - updated: expect.any(String), - first_name: 'John', - last_name: 'Doe', - full_name: 'John Doe', - birth_date: '1990-01-01T00:00:00.000Z', - age: expect.any(Number), - is_minor: false, - nickname: null, - promotion: null, - last_seen: expect.any(String), - }, - ]); + expect(response.body).toEqual({ + id: expect.any(Number), + created: expect.any(String), + updated: expect.any(String), + first_name: 'John', + last_name: 'Doe', + full_name: 'John Doe', + birth_date: '1990-01-01T00:00:00.000Z', + age: expect.any(Number), + is_minor: false, + nickname: null, + promotion: null, + last_seen: expect.any(String), + }); // restore user await em.persistAndFlush(user); @@ -449,9 +407,7 @@ describe('Users Data (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'abc'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'abc', property: 'id' }), }); }); }); @@ -496,9 +452,13 @@ describe('Users Data (e2e)', () => { }); describe('200 : Ok', () => { - it('when the use delete itself', async () => { + it('when the user delete itself', async () => { const user = await em.findOne(User, { email: fakeUserEmail }); + // Verify user before deleting it (otherwise it can't delete itself) + user.verified = new Date(); + await em.persistAndFlush(user); + const auth: res = await request(server).post('/auth/login').send({ email: user.email, password: user.password, @@ -511,8 +471,7 @@ describe('Users Data (e2e)', () => { .expect(200); expect(response.body).toEqual({ - statusCode: 200, - message: t.Success.Entity.Deleted(User), + ...new OutputMessageDTO('validations.user.success.deleted', { name: 'John Doe' }), }); }); }); @@ -527,9 +486,7 @@ describe('Users Data (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'abc'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'abc', property: 'id' }), }); }); }); @@ -581,9 +538,7 @@ describe('Users Data (e2e)', () => { .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - message: t.Errors.Id.NotFound(User, 9999), - statusCode: 404, + ...new i18nNotFoundException('validations.user.not_found.id', { id: 9999 }), }); }); }); @@ -610,7 +565,7 @@ describe('Users Data (e2e)', () => { last_seen: expect.any(String), secondary_email: null, phone: null, - parent_contact: null, + parents_phone: null, full_name: 'unverified user', age: expect.any(Number), is_minor: false, @@ -638,7 +593,7 @@ describe('Users Data (e2e)', () => { last_seen: expect.any(String), secondary_email: null, phone: null, - parent_contact: null, + parents_phone: null, full_name: 'root root', age: expect.any(Number), is_minor: false, @@ -656,9 +611,7 @@ describe('Users Data (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'abc'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'abc', property: 'id' }), }); }); }); @@ -697,9 +650,7 @@ describe('Users Data (e2e)', () => { .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - message: t.Errors.Id.NotFound(User, 9999), - statusCode: 404, + ...new i18nNotFoundException('validations.user.not_found.id', { id: 9999 }), }); }); }); @@ -729,21 +680,21 @@ describe('Users Data (e2e)', () => { it('when the user is asking for another user', async () => { const user = await request(server) - .get('/users/1/data/public') - .set('Authorization', `Bearer ${tokenSubscriber}`) + .get('/users/2/data/public') + .set('Authorization', `Bearer ${tokenRoot}`) .expect(200); expect(user.body).toEqual({ - id: 1, + id: 2, created: expect.any(String), updated: expect.any(String), - first_name: 'root', - last_name: 'root', + first_name: 'unverified', + last_name: 'user', birth_date: expect.any(String), - nickname: 'noot noot', - promotion: 21, + nickname: null, + promotion: null, last_seen: expect.any(String), - full_name: 'root root', + full_name: 'unverified user', age: expect.any(Number), is_minor: false, }); @@ -760,9 +711,7 @@ describe('Users Data (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'abc'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'abc', property: 'id' }), }); }); }); @@ -801,9 +750,7 @@ describe('Users Data (e2e)', () => { .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - message: t.Errors.Id.NotFounds(User, [9999]), - statusCode: 404, + ...new i18nNotFoundException('validations.user.not_found.id', { id: 9999 }), }); }); }); @@ -827,7 +774,7 @@ describe('Users Data (e2e)', () => { pronouns: false, promotion: true, phone: false, - parent_contact: false, + parents_phone: false, }); }); @@ -849,7 +796,7 @@ describe('Users Data (e2e)', () => { pronouns: false, promotion: true, phone: false, - parent_contact: false, + parents_phone: false, }); }); }); @@ -867,9 +814,7 @@ describe('Users Data (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'abc'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'abc', property: 'id' }), }); }); }); @@ -914,14 +859,12 @@ describe('Users Data (e2e)', () => { pronouns: false, promotion: true, phone: false, - parent_contact: false, + parents_phone: false, }) .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - message: t.Errors.Id.NotFound(User, 9999), - statusCode: 404, + ...new i18nNotFoundException('validations.user.not_found.id', { id: 9999 }), }); }); }); @@ -939,7 +882,7 @@ describe('Users Data (e2e)', () => { pronouns: false, promotion: true, phone: false, - parent_contact: false, + parents_phone: false, }) .expect(200); @@ -955,7 +898,7 @@ describe('Users Data (e2e)', () => { pronouns: false, promotion: true, phone: false, - parent_contact: false, + parents_phone: false, }); }); @@ -971,7 +914,7 @@ describe('Users Data (e2e)', () => { pronouns: false, promotion: true, phone: false, - parent_contact: false, + parents_phone: false, }) .expect(200); @@ -987,7 +930,7 @@ describe('Users Data (e2e)', () => { pronouns: false, promotion: true, phone: false, - parent_contact: false, + parents_phone: false, }); }); }); @@ -1002,9 +945,7 @@ describe('Users Data (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'abc'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'abc', property: 'id' }), }); }); }); @@ -1043,9 +984,7 @@ describe('Users Data (e2e)', () => { .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - message: t.Errors.Id.NotFound(User, 9999), - statusCode: 404, + ...new i18nNotFoundException('validations.user.not_found.id', { id: 9999 }), }); }); }); @@ -1110,9 +1049,7 @@ describe('Users Data (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'abc'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'abc', property: 'id' }), }); }); }); @@ -1151,9 +1088,7 @@ describe('Users Data (e2e)', () => { .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - message: t.Errors.Id.NotFound(User, 9999), - statusCode: 404, + ...new i18nNotFoundException('validations.user.not_found.id', { id: 9999 }), }); }); }); diff --git a/tests/e2e/users/users-files.e2e-spec.ts b/tests/e2e/users/users-files.e2e-spec.ts index 1024747d..45d9e9ab 100644 --- a/tests/e2e/users/users-files.e2e-spec.ts +++ b/tests/e2e/users/users-files.e2e-spec.ts @@ -1,15 +1,17 @@ -import { readFileSync } from 'fs'; +import { copyFileSync, existsSync, readFileSync, unlinkSync } from 'fs'; import { join } from 'path'; import request from 'supertest'; -import { TokenDTO } from '@modules/auth/dto/get.dto'; +import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; +import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { FileVisibilityGroup } from '@modules/files/entities/file-visibility.entity'; import { UserBanner } from '@modules/users/entities/user-banner.entity'; import { UserPicture } from '@modules/users/entities/user-picture.entity'; import { User } from '@modules/users/entities/user.entity'; -import { orm, server, t } from '../..'; +import { orm, server } from '../..'; describe('Users Files (e2e)', () => { let tokenUnauthorized: string; @@ -24,7 +26,7 @@ describe('Users Files (e2e)', () => { beforeAll(async () => { em = orm.em.fork(); - type res = Omit & { body: TokenDTO }; + type res = Omit & { body: OutputTokenDTO }; const responseA: res = await request(server).post('/auth/login').send({ email: 'unauthorized@email.com', @@ -57,6 +59,17 @@ describe('Users Files (e2e)', () => { }); describe('(GET) /users/:id/picture', () => { + const origin = join(process.cwd(), './tests/files/user_picture.jpeg'); + const copy = join(process.cwd(), './tests/files/user_picture_copy.jpeg'); + + beforeAll(() => { + copyFileSync(origin, copy); + }); + + afterAll(() => { + if (existsSync(copy)) unlinkSync(copy); + }); + describe('400 : Bad Request', () => { it('when the user id is invalid', async () => { const response = await request(server) @@ -65,9 +78,7 @@ describe('Users Files (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'invalid', property: 'id' }), }); }); }); @@ -81,34 +92,45 @@ describe('Users Files (e2e)', () => { message: 'Unauthorized', }); }); - }); - describe('403 : Forbidden', () => { - it('when the user is not authorized', async () => { + it('when the user is not in the visibility group of the picture', async () => { + const visibility_group = await em.findOne(FileVisibilityGroup, { name: 'SUBSCRIBER' }); + const picture = em.create(UserPicture, { + filename: 'user_picture.jpeg', + description: 'A fake picture for logs moderator', + mimetype: 'image/jpeg', + path: copy, + picture_user: em.getReference(User, 1), + size: 0, + visibility: visibility_group, + }); + + await em.persistAndFlush(picture); + const response = await request(server) .get('/users/1/picture') - .set('Authorization', `Bearer ${tokenUnauthorized}`) - .expect(403); + .set('Authorization', `Bearer ${tokenLogs}`) + .expect(401); expect(response.body).toEqual({ - error: 'Forbidden', - statusCode: 403, - message: 'Forbidden resource', + ...new i18nUnauthorizedException('validations.user.invalid.not_in_file_visibility_group', { + group_name: 'SUBSCRIBER', + }), }); + + await em.removeAndFlush(picture); }); }); describe('404 : Not Found', () => { it('when the user does not exist', async () => { const response = await request(server) - .get('/users/999/picture') + .get('/users/9999/picture') .set('Authorization', `Bearer ${tokenRoot}`) .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Id.NotFound(User, 999), + ...new i18nNotFoundException('validations.user.not_found.id', { id: 9999 }), }); }); @@ -119,9 +141,7 @@ describe('Users Files (e2e)', () => { .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.User.NoPicture(1), + ...new i18nNotFoundException('validations.user.picture.not_found', { name: 'root root' }), }); }); }); @@ -136,7 +156,7 @@ describe('Users Files (e2e)', () => { filename: 'user_picture.jpeg', description: 'A fake picture for logs moderator', mimetype: 'image/jpeg', - path: join(process.cwd(), './tests/files/user_picture.jpeg'), + path: copy, picture_user: em.getReference(User, idLogs), size: 0, visibility: visibility_group, @@ -146,7 +166,6 @@ describe('Users Files (e2e)', () => { }); afterAll(async () => { - // Delete the fake picture await em.removeAndFlush(picture); }); @@ -183,6 +202,17 @@ describe('Users Files (e2e)', () => { }); describe('(POST) /users/:id/picture', () => { + const origin = join(process.cwd(), './tests/files/user_picture.jpeg'); + const copy = join(process.cwd(), './tests/files/user_picture_copy.jpeg'); + + beforeAll(() => { + copyFileSync(origin, copy); + }); + + afterAll(() => { + if (existsSync(copy)) unlinkSync(copy); + }); + describe('400 : Bad Request', () => { it('when the user id is invalid', async () => { const response = await request(server) @@ -192,9 +222,7 @@ describe('Users Files (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'invalid', property: 'id' }), }); }); @@ -205,9 +233,7 @@ describe('Users Files (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.File.NotProvided(), + ...new i18nBadRequestException('validations.file.invalid.not_provided'), }); }); }); @@ -231,7 +257,7 @@ describe('Users Files (e2e)', () => { filename: 'user_picture.jpeg', description: 'A fake picture for root', mimetype: 'image/jpeg', - path: join(process.cwd(), './tests/files/user_picture.jpeg'), + path: copy, picture_user: em.getReference(User, idLogs), size: 0, visibility: await em.findOneOrFail(FileVisibilityGroup, { name: 'SUBSCRIBER' }), @@ -247,11 +273,7 @@ describe('Users Files (e2e)', () => { .expect(401); expect(response.body).toEqual({ - error: 'Unauthorized', - statusCode: 401, - // TODO: (KEY: 8) Find a way to get the cooldown and test the message (time is not the same) - // message: t.Errors.User.PictureCooldown(picture.updated.getTime() + env().users.picture_cooldown - Date.now()), - message: expect.any(String), + ...new i18nUnauthorizedException('validations.user.picture.cooldown', { days: 7 }), }); // Delete the fake picture @@ -278,15 +300,13 @@ describe('Users Files (e2e)', () => { describe('404 : Not Found', () => { it('when the user does not exist', async () => { const response = await request(server) - .post('/users/999/picture') + .post('/users/9999/picture') .set('Authorization', `Bearer ${tokenRoot}`) .attach('file', filePictureSquare, 'file.jpeg') .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Id.NotFound(User, 999), + ...new i18nNotFoundException('validations.user.not_found.id', { id: 9999 }), }); }); }); @@ -296,8 +316,8 @@ describe('Users Files (e2e)', () => { const response = await request(server) .post('/users/4/picture') .set('Authorization', `Bearer ${tokenLogs}`) - .attach('file', filePictureSquare, 'file.jpeg') - .expect(201); + .attach('file', filePictureSquare, 'file.jpeg'); + // .expect(201); expect(response.body).toEqual({ created: expect.any(String), @@ -380,9 +400,7 @@ describe('Users Files (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'invalid', property: 'id' }), }); }); }); @@ -429,14 +447,12 @@ describe('Users Files (e2e)', () => { describe('404 : Not Found', () => { it('when the user does not exist', async () => { const response = await request(server) - .delete('/users/999/picture') + .delete('/users/9999/picture') .set('Authorization', `Bearer ${tokenRoot}`) .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Id.NotFound(User, 999), + ...new i18nNotFoundException('validations.user.not_found.id', { id: 9999 }), }); }); @@ -447,9 +463,7 @@ describe('Users Files (e2e)', () => { .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.User.NoPicture(1), + ...new i18nNotFoundException('validations.user.picture.not_found', { name: 'root root' }), }); }); }); @@ -470,14 +484,24 @@ describe('Users Files (e2e)', () => { .expect(200); expect(response.body).toEqual({ - message: t.Success.Entity.Deleted(UserPicture), - statusCode: 201, + ...new OutputMessageDTO('validations.user.success.deleted_picture', { name: 'root root' }), }); }); }); }); describe('(GET) /users/:id/banner', () => { + const origin = join(process.cwd(), './tests/files/user_banner.jpeg'); + const copy = join(process.cwd(), './tests/files/user_banner_copy.jpeg'); + + beforeAll(() => { + copyFileSync(origin, copy); + }); + + afterAll(() => { + if (existsSync(copy)) unlinkSync(copy); + }); + describe('400 : Bad Request', () => { it('when the user id is invalid', async () => { const response = await request(server) @@ -486,9 +510,7 @@ describe('Users Files (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'invalid', property: 'id' }), }); }); }); @@ -504,21 +526,6 @@ describe('Users Files (e2e)', () => { }); }); - describe('403 : Forbidden', () => { - it('when the user is not authorized', async () => { - const response = await request(server) - .get('/users/1/banner') - .set('Authorization', `Bearer ${tokenUnauthorized}`) - .expect(403); - - expect(response.body).toEqual({ - error: 'Forbidden', - statusCode: 403, - message: 'Forbidden resource', - }); - }); - }); - describe('404 : Not Found', () => { it('when the user does not exist', async () => { const response = await request(server) @@ -527,9 +534,7 @@ describe('Users Files (e2e)', () => { .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Id.NotFound(User, 999), + ...new i18nNotFoundException('validations.user.not_found.id', { id: 999 }), }); }); @@ -540,9 +545,7 @@ describe('Users Files (e2e)', () => { .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.User.NoBanner(1), + ...new i18nNotFoundException('validations.user.banner.not_found', { name: 'root root' }), }); }); }); @@ -557,7 +560,7 @@ describe('Users Files (e2e)', () => { filename: 'user_banner.jpeg', description: 'A fake banner for logs moderator', mimetype: 'image/jpeg', - path: join(process.cwd(), './tests/files/user_banner.jpeg'), + path: copy, banner_user: em.getReference(User, idLogs), size: 0, visibility: visibility_group, @@ -613,9 +616,7 @@ describe('Users Files (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'invalid', property: 'id' }), }); }); @@ -626,9 +627,7 @@ describe('Users Files (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.File.NotProvided(), + ...new i18nBadRequestException('validations.file.invalid.not_provided'), }); }); }); @@ -672,9 +671,7 @@ describe('Users Files (e2e)', () => { .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Id.NotFound(User, 999), + ...new i18nNotFoundException('validations.user.not_found.id', { id: 999 }), }); }); }); @@ -768,9 +765,7 @@ describe('Users Files (e2e)', () => { .expect(400); expect(response.body).toEqual({ - error: 'Bad Request', - statusCode: 400, - message: t.Errors.Id.Invalid(User, 'invalid'), + ...new i18nBadRequestException('validations.id.invalid.format', { value: 'invalid', property: 'id' }), }); }); }); @@ -809,9 +804,7 @@ describe('Users Files (e2e)', () => { .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.Id.NotFound(User, 999), + ...new i18nNotFoundException('validations.user.not_found.id', { id: 999 }), }); }); @@ -822,9 +815,7 @@ describe('Users Files (e2e)', () => { .expect(404); expect(response.body).toEqual({ - error: 'Not Found', - statusCode: 404, - message: t.Errors.User.NoBanner(1), + ...new i18nNotFoundException('validations.user.banner.not_found', { name: 'root root' }), }); }); }); @@ -845,8 +836,7 @@ describe('Users Files (e2e)', () => { .expect(200); expect(response.body).toEqual({ - message: t.Success.Entity.Deleted(UserBanner), - statusCode: 201, + ...new OutputMessageDTO('validations.user.success.deleted_banner', { name: 'root root' }), }); }); }); diff --git a/tests/index.ts b/tests/index.ts index f7afce87..5705b07d 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -7,15 +7,16 @@ import { MikroORM } from '@mikro-orm/core'; import { JwtService } from '@nestjs/jwt'; import { NestExpressApplication } from '@nestjs/platform-express'; import { TestingModule, Test } from '@nestjs/testing'; +import { I18nContext, I18nService, I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n'; import { AppModule } from '@app.module'; -import { TranslateService } from '@modules/translate/translate.service'; +import { VALIDATION_PIPE_OPTIONS } from '@env'; +import { I18nHttpExceptionFilter } from '@modules/_mixin/http-errors'; let module_fixture: TestingModule; let jwt: JwtService; let app: NestExpressApplication; let server: Awaited>; -let t: TranslateService; /** Should be forked using om.em.fork() for each test suite */ let orm: MikroORM; @@ -33,11 +34,15 @@ beforeAll(async () => { app = module_fixture.createNestApplication(); app.enableCors({ origin: '*' }); + app.useGlobalPipes(new I18nValidationPipe(VALIDATION_PIPE_OPTIONS)); + app.useGlobalFilters(new I18nValidationExceptionFilter({ detailedErrors: false }), new I18nHttpExceptionFilter()); orm = module_fixture.get(MikroORM); - t = module_fixture.get(TranslateService); jwt = module_fixture.get(JwtService); + const i18nService = app.get(I18nService); + jest.spyOn(I18nContext, 'current').mockImplementation(() => new I18nContext('en-US', i18nService)); + server = await app.listen(5325); await app.init(); }); @@ -49,4 +54,7 @@ afterAll(async () => { server.close(); }); +/** @deprecated */ +const t = {}; + export { module_fixture, server, orm, t, jwt }; diff --git a/tests/units/services/auth.test.ts b/tests/units/services/auth.test.ts index 23fec9fd..853be2de 100644 --- a/tests/units/services/auth.test.ts +++ b/tests/units/services/auth.test.ts @@ -1,9 +1,8 @@ -import { UnauthorizedException } from '@nestjs/common'; - import { env } from '@env'; +import { i18nUnauthorizedException } from '@modules/_mixin/http-errors'; import { AuthService } from '@modules/auth/auth.service'; -import { module_fixture, jwt, t } from '../..'; +import { module_fixture, jwt } from '../..'; describe('AuthService (unit)', () => { let authService: AuthService; @@ -15,14 +14,14 @@ describe('AuthService (unit)', () => { describe('.verifyJWT()', () => { it('should return an error if the token is invalid', () => { expect(() => authService.verifyJWT('Bearer invalid')).toThrowError( - new UnauthorizedException(t.Errors.JWT.Invalid()), + new i18nUnauthorizedException('validations.token.invalid.format', { property: 'token', value: 'invalid' }), ); }); it('should return an error if the token is expired', () => { expect(() => authService.verifyJWT(jwt.sign({ id: 1, email: 'test@example.fr' }, { expiresIn: '0s', secret: env.JWT_KEY })), - ).toThrowError(new UnauthorizedException(t.Errors.JWT.Expired())); + ).toThrowError(new i18nUnauthorizedException('validations.token.invalid.expired')); }); }); }); diff --git a/tests/units/services/files.test.ts b/tests/units/services/files.test.ts index 4c272d85..b123a70a 100644 --- a/tests/units/services/files.test.ts +++ b/tests/units/services/files.test.ts @@ -1,11 +1,9 @@ -import { BadRequestException, NotFoundException } from '@nestjs/common'; - -import { FileVisibilityGroup } from '@modules/files/entities/file-visibility.entity'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/_mixin/http-errors'; import { FilesService } from '@modules/files/files.service'; import { PromotionPicture } from '@modules/promotions/entities/promotion-picture.entity'; import { Promotion } from '@modules/promotions/entities/promotion.entity'; -import { orm, module_fixture, t } from '../..'; +import { orm, module_fixture } from '../..'; describe('FilesService (unit)', () => { let filesService: FilesService; @@ -44,7 +42,7 @@ describe('FilesService (unit)', () => { it('should throw when the file cannot be accessed', () => { expect(() => { filesService.toReadable(fake_file); - }).toThrow(new NotFoundException(t.Errors.File.NotFoundOnDisk(fake_file.filename))); + }).toThrow(new i18nNotFoundException('validations.file.invalid.not_found', { filename: fake_file.filename })); }); }); @@ -56,30 +54,18 @@ describe('FilesService (unit)', () => { it('should throw when asked if the file does not exist', () => { expect(() => { filesService.deleteFromDisk(fake_file, false); - }).toThrow(new NotFoundException(t.Errors.File.NotFoundOnDisk(fake_file.filename))); + }).toThrow(new i18nNotFoundException('validations.file.invalid.not_found', { filename: fake_file.filename })); }); }); describe('.getVisibilityGroup()', () => { it('should throw a bad request exception when the visibility group does not exist', async () => { await expect(filesService.getVisibilityGroup('FOO_BAR')).rejects.toThrow( - new BadRequestException(t.Errors.Entity.NotFound(FileVisibilityGroup, 'FOO_BAR', 'name')), + new i18nBadRequestException('validations.file_visibility_group.invalid.not_found', { name: 'FOO_BAR' }), ); }); }); - describe('.writeOnDiskAsImage()', () => { - it('should throw if nof file is provided', async () => { - await expect( - filesService.writeOnDiskAsImage(undefined, { - directory: 'string', - filename: 'string', - aspect_ratio: '1:1', - }), - ).rejects.toThrow(new BadRequestException(t.Errors.File.NotProvided())); - }); - }); - describe('.writeOnDisk()', () => { it('should throw if no file is provided', async () => { await expect( @@ -91,7 +77,7 @@ describe('FilesService (unit)', () => { }, ['image/png'], ), - ).rejects.toThrow(new BadRequestException(t.Errors.File.NotProvided())); + ).rejects.toThrow(new i18nBadRequestException('validations.file.invalid.not_provided')); }); }); }); diff --git a/tests/units/services/images.test.ts b/tests/units/services/images.test.ts new file mode 100644 index 00000000..b6a3b311 --- /dev/null +++ b/tests/units/services/images.test.ts @@ -0,0 +1,24 @@ +import { i18nBadRequestException } from '@modules/_mixin/http-errors'; +import { ImagesService } from '@modules/files/images.service'; + +import { module_fixture } from '../..'; + +describe('ImagesService (unit)', () => { + let imagesService: ImagesService; + + beforeAll(() => { + imagesService = module_fixture.get(ImagesService); + }); + + describe('.writeOnDisk()', () => { + it('should throw if no file is provided', async () => { + await expect( + imagesService.writeOnDisk(undefined, { + directory: 'string', + filename: 'string', + aspect_ratio: '1:1', + }), + ).rejects.toThrow(new i18nBadRequestException('validations.file.invalid.not_provided')); + }); + }); +}); diff --git a/tests/units/services/users/users-data.test.ts b/tests/units/services/users/users-data.test.ts index 6a7589ab..e3e10b6f 100644 --- a/tests/units/services/users/users-data.test.ts +++ b/tests/units/services/users/users-data.test.ts @@ -1,5 +1,6 @@ import { hashSync } from 'bcrypt'; +import { i18nNotFoundException } from '@modules/_mixin/http-errors'; import { Permission } from '@modules/permissions/entities/permission.entity'; import { Role } from '@modules/roles/entities/role.entity'; import { User } from '@modules/users/entities/user.entity'; @@ -95,4 +96,28 @@ describe('UsersDataService (unit)', () => { expect(res).toBe(false); }); }); + + describe('.findOne()', () => { + it('should throw if no user is found', async () => { + await expect(usersDataService.findOne(999999, false)).rejects.toThrow( + new i18nNotFoundException('validations.user.not_found.id', { id: 999999 }), + ); + + await expect(usersDataService.findOne('email@email.com', false)).rejects.toThrow( + new i18nNotFoundException('validations.user.not_found.email', { email: 'email@email.com' }), + ); + }); + }); + + describe('.findVisibilities()', () => { + it('should throw if no user visibilities are found', async () => { + await expect(usersDataService.findVisibilities(999999)).rejects.toThrow( + new i18nNotFoundException('validations.user.not_found.id', { id: 999999 }), + ); + + await expect(usersDataService.findVisibilities([99999, 9999])).rejects.toThrow( + new i18nNotFoundException('validations.users.not_found.ids', { ids: [99999, 9999].join("', '") }), + ); + }); + }); }); diff --git a/tests/units/utils/password.test.ts b/tests/units/utils/password.test.ts index 5043050f..3b015375 100644 --- a/tests/units/utils/password.test.ts +++ b/tests/units/utils/password.test.ts @@ -1,4 +1,4 @@ -import { generateRandomPassword, checkPasswordStrength } from '@utils/password'; +import { generateRandomPassword } from '@modules/_mixin/decorators/is-strong-pass.decorator'; describe('Password (unit)', () => { describe('.generateRandomPassword()', () => { @@ -7,20 +7,9 @@ describe('Password (unit)', () => { expect(generateRandomPassword(5)).toHaveLength(8); expect(generateRandomPassword(8)).toHaveLength(8); - for (let i = 8; i < 64; i++) { + for (let i = 8; i < 16; i++) { expect(generateRandomPassword(i)).toHaveLength(i); } }); }); - - describe('.checkPasswordStrength()', () => { - it('should return true if the password is strong enough', () => { - expect(checkPasswordStrength(generateRandomPassword(5))).toBe(true); - expect(checkPasswordStrength(generateRandomPassword(8))).toBe(true); - - for (let i = 8; i < 512; i++) { - expect(checkPasswordStrength(generateRandomPassword(i))).toBe(true); - } - }); - }); }); From 3414ada33f548e3b5ff4faa8b3ff79e2cae987ad Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Tue, 7 Nov 2023 18:32:48 +0100 Subject: [PATCH 06/15] fix(tests): setup treshold for tests validations to 95% --- jest.config.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index 03f4ec09..bbb1ce47 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -4,20 +4,21 @@ const config: JestConfigWithTsJest = { coverageReporters: ['text', 'lcov'], collectCoverage: true, coverageDirectory: 'coverage', - // coverageThreshold: { - // global: { - // branches: 100, - // functions: 100, - // lines: 100, - // statements: 100, - // }, - // }, + coverageThreshold: { + global: { + branches: 95, + functions: 95, + lines: 95, + statements: 95, + }, + }, detectOpenHandles: true, maxConcurrency: 1, moduleNameMapper: { '@env': '/src/env.ts', '@mikro-orm.config': '/src/mikro-orm.config.ts', '@app.module': '/src/app.module.ts', + '@main': '/src/main.ts', '^src/(.*)$': '/src/$1', '^@database/(.*)$': '/src/database/$1', '^@exported/(.*)$': '/src/exported/$1', From 4c6cbb93ed328a797fd054cd7c8e4a63677c3dcd Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Tue, 7 Nov 2023 18:33:17 +0100 Subject: [PATCH 07/15] core(update): updated deps & README --- README.md | 5 +- package.json | 55 +- pnpm-lock.yaml | 1321 +++++++++++++++++++++++++----------------------- 3 files changed, 709 insertions(+), 672 deletions(-) diff --git a/README.md b/README.md index 8e246f33..40bbdda7 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,11 @@ pnpm run start:prod Both unit and e2e tests are available and run with [Jest](https://jestjs.io/). You can run them with the following command: ```bash -# unit tests +# all tests pnpm test + +# unique test file +pnpm test -- "auth.e2e-spec.ts" ``` > After running the tests, a coverage report is generated in the `./coverage` folder. diff --git a/package.json b/package.json index 7ab6ba08..84e18cb5 100644 --- a/package.json +++ b/package.json @@ -23,57 +23,58 @@ }, "dependencies": { "@ae_utbm/typings": "file:src/exported", - "@mikro-orm/cli": "^5.8.9", - "@mikro-orm/core": "^5.8.9", - "@mikro-orm/migrations": "^5.8.9", + "@mikro-orm/cli": "^5.9.1", + "@mikro-orm/core": "^5.9.1", + "@mikro-orm/migrations": "^5.9.1", "@mikro-orm/nestjs": "^5.2.2", - "@mikro-orm/postgresql": "^5.8.9", - "@mikro-orm/reflection": "^5.8.9", - "@mikro-orm/seeder": "^5.8.9", + "@mikro-orm/postgresql": "^5.9.1", + "@mikro-orm/reflection": "^5.9.1", + "@mikro-orm/seeder": "^5.9.1", "@mikro-orm/sql-highlighter": "^1.0.1", - "@nestjs/common": "^10.2.7", + "@nestjs/common": "^10.2.8", "@nestjs/config": "^3.1.1", - "@nestjs/core": "^10.2.7", + "@nestjs/core": "^10.2.8", "@nestjs/jwt": "^10.1.1", "@nestjs/passport": "^10.0.2", - "@nestjs/platform-express": "^10.2.7", + "@nestjs/platform-express": "^10.2.8", "@nestjs/schedule": "^3.0.4", - "@nestjs/swagger": "^7.1.13", + "@nestjs/swagger": "^7.1.14", "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "dotenv": "^16.3.1", "express": "^4.18.2", "file-type": "^16.5.4", "jsonwebtoken": "^9.0.2", - "nestjs-i18n": "^10.3.6", - "nodemailer": "^6.9.6", + "nestjs-i18n": "^10.3.7", + "nodemailer": "^6.9.7", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "sharp": "^0.32.6", - "type-fest": "^4.4.0", + "type-fest": "^4.6.0", "zod": "^3.22.4" }, "devDependencies": { - "@nestjs/cli": "^10.1.18", - "@nestjs/schematics": "^10.0.2", - "@nestjs/testing": "^10.2.7", - "@types/bcrypt": "^5.0.0", - "@types/express": "^4.17.19", + "@nestjs/cli": "^10.2.1", + "@nestjs/schematics": "^10.0.3", + "@nestjs/testing": "^10.2.8", + "@types/bcrypt": "^5.0.1", + "@types/express": "^4.17.20", "@types/jest": "29.5.2", - "@types/jsonwebtoken": "^9.0.3", - "@types/multer": "^1.4.8", + "@types/jsonwebtoken": "^9.0.4", + "@types/multer": "^1.4.9", "@types/node": "20.5.7", - "@types/nodemailer": "^6.4.11", - "@types/passport-jwt": "^3.0.10", - "@types/supertest": "^2.0.14", - "@typescript-eslint/eslint-plugin": "^6.8.0", - "@typescript-eslint/parser": "^6.8.0", - "eslint": "^8.51.0", + "@types/nodemailer": "^6.4.13", + "@types/passport-jwt": "^3.0.12", + "@types/supertest": "^2.0.15", + "@typescript-eslint/eslint-plugin": "^6.9.1", + "@typescript-eslint/parser": "^6.9.1", + "eslint": "^8.52.0", "eslint-config-prettier": "^9.0.0", "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.28.1", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-prettier": "^4.2.1", "jest": "^29.7.0", "jest-extended": "^4.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f904639b..eb654b66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,56 +9,59 @@ dependencies: specifier: file:src/exported version: file:src/exported '@mikro-orm/cli': - specifier: ^5.8.9 - version: 5.8.9(@mikro-orm/migrations@5.8.9)(@mikro-orm/postgresql@5.8.9)(@mikro-orm/seeder@5.8.9)(pg@8.11.3) + specifier: ^5.9.1 + version: 5.9.1(@mikro-orm/migrations@5.9.1)(@mikro-orm/postgresql@5.9.1)(@mikro-orm/seeder@5.9.1)(pg@8.11.3) '@mikro-orm/core': - specifier: ^5.8.9 - version: 5.8.9(@mikro-orm/migrations@5.8.9)(@mikro-orm/postgresql@5.8.9)(@mikro-orm/seeder@5.8.9) + specifier: ^5.9.1 + version: 5.9.1(@mikro-orm/migrations@5.9.1)(@mikro-orm/postgresql@5.9.1)(@mikro-orm/seeder@5.9.1) '@mikro-orm/migrations': - specifier: ^5.8.9 - version: 5.8.9(@mikro-orm/core@5.8.9)(pg@8.11.3) + specifier: ^5.9.1 + version: 5.9.1(@mikro-orm/core@5.9.1)(pg@8.11.3) '@mikro-orm/nestjs': specifier: ^5.2.2 - version: 5.2.2(@mikro-orm/core@5.8.9)(@nestjs/common@10.2.7)(@nestjs/core@10.2.7) + version: 5.2.2(@mikro-orm/core@5.9.1)(@nestjs/common@10.2.8)(@nestjs/core@10.2.8) '@mikro-orm/postgresql': - specifier: ^5.8.9 - version: 5.8.9(@mikro-orm/core@5.8.9)(@mikro-orm/migrations@5.8.9)(@mikro-orm/seeder@5.8.9) + specifier: ^5.9.1 + version: 5.9.1(@mikro-orm/core@5.9.1)(@mikro-orm/migrations@5.9.1)(@mikro-orm/seeder@5.9.1) '@mikro-orm/reflection': - specifier: ^5.8.9 - version: 5.8.9(@mikro-orm/core@5.8.9) + specifier: ^5.9.1 + version: 5.9.1(@mikro-orm/core@5.9.1) '@mikro-orm/seeder': - specifier: ^5.8.9 - version: 5.8.9(@mikro-orm/core@5.8.9) + specifier: ^5.9.1 + version: 5.9.1(@mikro-orm/core@5.9.1) '@mikro-orm/sql-highlighter': specifier: ^1.0.1 version: 1.0.1 '@nestjs/common': - specifier: ^10.2.7 - version: 10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: ^10.2.8 + version: 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/config': specifier: ^3.1.1 - version: 3.1.1(@nestjs/common@10.2.7)(reflect-metadata@0.1.13) + version: 3.1.1(@nestjs/common@10.2.8)(reflect-metadata@0.1.13) '@nestjs/core': - specifier: ^10.2.7 - version: 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: ^10.2.8 + version: 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/jwt': specifier: ^10.1.1 - version: 10.1.1(@nestjs/common@10.2.7) + version: 10.1.1(@nestjs/common@10.2.8) '@nestjs/passport': specifier: ^10.0.2 - version: 10.0.2(@nestjs/common@10.2.7)(passport@0.6.0) + version: 10.0.2(@nestjs/common@10.2.8)(passport@0.6.0) '@nestjs/platform-express': - specifier: ^10.2.7 - version: 10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7) + specifier: ^10.2.8 + version: 10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8) '@nestjs/schedule': specifier: ^3.0.4 - version: 3.0.4(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(reflect-metadata@0.1.13) + version: 3.0.4(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(reflect-metadata@0.1.13) '@nestjs/swagger': - specifier: ^7.1.13 - version: 7.1.13(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(class-validator@0.14.0)(reflect-metadata@0.1.13) + specifier: ^7.1.14 + version: 7.1.14(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) bcrypt: specifier: ^5.1.1 version: 5.1.1 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 class-validator: specifier: ^0.14.0 version: 0.14.0 @@ -75,11 +78,11 @@ dependencies: specifier: ^9.0.2 version: 9.0.2 nestjs-i18n: - specifier: ^10.3.6 - version: 10.3.6(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(class-validator@0.14.0)(rxjs@7.8.1) + specifier: ^10.3.7 + version: 10.3.7(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(class-validator@0.14.0)(rxjs@7.8.1) nodemailer: - specifier: ^6.9.6 - version: 6.9.6 + specifier: ^6.9.7 + version: 6.9.7 passport-jwt: specifier: ^4.0.1 version: 4.0.1 @@ -96,70 +99,70 @@ dependencies: specifier: ^0.32.6 version: 0.32.6 type-fest: - specifier: ^4.4.0 - version: 4.4.0 + specifier: ^4.6.0 + version: 4.6.0 zod: specifier: ^3.22.4 version: 3.22.4 devDependencies: '@nestjs/cli': - specifier: ^10.1.18 - version: 10.1.18 + specifier: ^10.2.1 + version: 10.2.1 '@nestjs/schematics': - specifier: ^10.0.2 - version: 10.0.2(chokidar@3.5.3)(typescript@5.2.2) + specifier: ^10.0.3 + version: 10.0.3(chokidar@3.5.3)(typescript@5.2.2) '@nestjs/testing': - specifier: ^10.2.7 - version: 10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(@nestjs/platform-express@10.2.7) + specifier: ^10.2.8 + version: 10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(@nestjs/platform-express@10.2.8) '@types/bcrypt': - specifier: ^5.0.0 - version: 5.0.0 + specifier: ^5.0.1 + version: 5.0.1 '@types/express': - specifier: ^4.17.19 - version: 4.17.19 + specifier: ^4.17.20 + version: 4.17.20 '@types/jest': specifier: 29.5.2 version: 29.5.2 '@types/jsonwebtoken': - specifier: ^9.0.3 - version: 9.0.3 + specifier: ^9.0.4 + version: 9.0.4 '@types/multer': - specifier: ^1.4.8 - version: 1.4.8 + specifier: ^1.4.9 + version: 1.4.9 '@types/node': specifier: 20.5.7 version: 20.5.7 '@types/nodemailer': - specifier: ^6.4.11 - version: 6.4.11 + specifier: ^6.4.13 + version: 6.4.13 '@types/passport-jwt': - specifier: ^3.0.10 - version: 3.0.10 + specifier: ^3.0.12 + version: 3.0.12 '@types/supertest': - specifier: ^2.0.14 - version: 2.0.14 + specifier: ^2.0.15 + version: 2.0.15 '@typescript-eslint/eslint-plugin': - specifier: ^6.8.0 - version: 6.8.0(@typescript-eslint/parser@6.8.0)(eslint@8.51.0)(typescript@5.2.2) + specifier: ^6.9.1 + version: 6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.52.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: ^6.8.0 - version: 6.8.0(eslint@8.51.0)(typescript@5.2.2) + specifier: ^6.9.1 + version: 6.9.1(eslint@8.52.0)(typescript@5.2.2) eslint: - specifier: ^8.51.0 - version: 8.51.0 + specifier: ^8.52.0 + version: 8.52.0 eslint-config-prettier: specifier: ^9.0.0 - version: 9.0.0(eslint@8.51.0) + version: 9.0.0(eslint@8.52.0) eslint-import-resolver-typescript: specifier: ^3.6.1 - version: 3.6.1(@typescript-eslint/parser@6.8.0)(eslint-plugin-import@2.28.1)(eslint@8.51.0) + version: 3.6.1(@typescript-eslint/parser@6.9.1)(eslint-plugin-import@2.29.0)(eslint@8.52.0) eslint-plugin-import: - specifier: ^2.28.1 - version: 2.28.1(@typescript-eslint/parser@6.8.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.51.0) + specifier: ^2.29.0 + version: 2.29.0(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) eslint-plugin-prettier: specifier: ^4.2.1 - version: 4.2.1(eslint-config-prettier@9.0.0)(eslint@8.51.0)(prettier@2.8.8) + version: 4.2.1(eslint-config-prettier@9.0.0)(eslint@8.52.0)(prettier@2.8.8) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.5.7)(ts-node@10.9.1) @@ -203,28 +206,11 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.20 dev: true - /@angular-devkit/core@16.1.8(chokidar@3.5.3): - resolution: {integrity: sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==} - engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - peerDependencies: - chokidar: ^3.5.2 - peerDependenciesMeta: - chokidar: - optional: true - dependencies: - ajv: 8.12.0 - ajv-formats: 2.1.1(ajv@8.12.0) - chokidar: 3.5.3 - jsonc-parser: 3.2.0 - rxjs: 7.8.1 - source-map: 0.7.4 - dev: true - - /@angular-devkit/core@16.2.3(chokidar@3.5.3): - resolution: {integrity: sha512-oZLdg2XTx7likYAXRj1CU0XmrsCfe5f2grj3iwuI3OB1LXwwpdbHBztruj03y3yHES+TnO+dIbkvRnvMXs7uAA==} + /@angular-devkit/core@16.2.8(chokidar@3.5.3): + resolution: {integrity: sha512-PTGozYvh1Bin5lB15PwcXa26Ayd17bWGLS3H8Rs0s+04mUDvfNofmweaX1LgumWWy3nCUTDuwHxX10M3G0wE2g==} engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: chokidar: ^3.5.2 @@ -241,13 +227,13 @@ packages: source-map: 0.7.4 dev: true - /@angular-devkit/schematics-cli@16.2.3(chokidar@3.5.3): - resolution: {integrity: sha512-5YQCbQmY9Kc03a9Io4XHOrxGXjnzcVveUuUO64R1m5x2aA5I+mVR8NVvxuoGRAeoI1FWusAKRe9hH8nRCLrelA==} + /@angular-devkit/schematics-cli@16.2.8(chokidar@3.5.3): + resolution: {integrity: sha512-EXURJCzWTVYCipiTT4vxQQOrF63asOUDbeOy3OtiSh7EwIUvxm3BPG6hquJqngEnI/N6bA75NJ1fBhU6Hrh7eA==} engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true dependencies: - '@angular-devkit/core': 16.2.3(chokidar@3.5.3) - '@angular-devkit/schematics': 16.2.3(chokidar@3.5.3) + '@angular-devkit/core': 16.2.8(chokidar@3.5.3) + '@angular-devkit/schematics': 16.2.8(chokidar@3.5.3) ansi-colors: 4.1.3 inquirer: 8.2.4 symbol-observable: 4.0.0 @@ -256,24 +242,11 @@ packages: - chokidar dev: true - /@angular-devkit/schematics@16.1.8(chokidar@3.5.3): - resolution: {integrity: sha512-6LyzMdFJs337RTxxkI2U1Ndw0CW5mMX/aXWl8d7cW2odiSrAg8IdlMqpc+AM8+CPfsB0FtS1aWkEZqJLT0jHOg==} + /@angular-devkit/schematics@16.2.8(chokidar@3.5.3): + resolution: {integrity: sha512-MBiKZOlR9/YMdflALr7/7w/BGAfo/BGTrlkqsIB6rDWV1dYiCgxI+033HsiNssLS6RQyCFx/e7JA2aBBzu9zEg==} engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} dependencies: - '@angular-devkit/core': 16.1.8(chokidar@3.5.3) - jsonc-parser: 3.2.0 - magic-string: 0.30.0 - ora: 5.4.1 - rxjs: 7.8.1 - transitivePeerDependencies: - - chokidar - dev: true - - /@angular-devkit/schematics@16.2.3(chokidar@3.5.3): - resolution: {integrity: sha512-+lBiHxi/C9HCfiCbtW25DldwvJDXXXv5oWw+Tg4s18BO/lYZLveGUEaZWu9ZJ5VIJ8GliUi2LohxhDxBkh4Oxg==} - engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - dependencies: - '@angular-devkit/core': 16.2.3(chokidar@3.5.3) + '@angular-devkit/core': 16.2.8(chokidar@3.5.3) jsonc-parser: 3.2.0 magic-string: 0.30.1 ora: 5.4.1 @@ -324,7 +297,7 @@ packages: dependencies: '@babel/types': 7.23.0 '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.20 jsesc: 2.5.2 dev: true @@ -625,18 +598,18 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.51.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.52.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.51.0 + eslint: 8.52.0 eslint-visitor-keys: 3.4.3 dev: true - /@eslint-community/regexpp@4.9.1: - resolution: {integrity: sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==} + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true @@ -657,8 +630,8 @@ packages: - supports-color dev: true - /@eslint/js@8.51.0: - resolution: {integrity: sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==} + /@eslint/js@8.52.0: + resolution: {integrity: sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -667,11 +640,11 @@ packages: engines: {node: '>=14.0.0', npm: '>=6.0.0'} dev: false - /@humanwhocodes/config-array@0.11.11: - resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} + /@humanwhocodes/config-array@0.11.13: + resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} engines: {node: '>=10.10.0'} dependencies: - '@humanwhocodes/object-schema': 1.2.1 + '@humanwhocodes/object-schema': 2.0.1 debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: @@ -683,8 +656,20 @@ packages: engines: {node: '>=12.22'} dev: true - /@humanwhocodes/object-schema@1.2.1: - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + /@humanwhocodes/object-schema@2.0.1: + resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + dev: true + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 dev: true /@istanbuljs/load-nyc-config@1.1.0: @@ -707,7 +692,7 @@ packages: resolution: {integrity: sha512-zBp2myVvBHp1UaJsNTyS6q4UDKT7eRiqTS4oNTS6VQMd6mpxYOdbeK4pY279cDCdakGy6hG0J3ejoXZVsPwHqw==} dependencies: chalk: 4.1.2 - figlet: 1.6.0 + figlet: 1.7.0 parent-require: 1.0.0 dev: false @@ -831,7 +816,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.20 '@types/node': 20.5.7 chalk: 4.1.2 collect-v8-coverage: 1.0.2 @@ -865,7 +850,7 @@ packages: resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.20 callsites: 3.1.0 graceful-fs: 4.2.11 dev: true @@ -876,7 +861,7 @@ packages: dependencies: '@jest/console': 29.7.0 '@jest/types': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-lib-coverage': 2.0.5 collect-v8-coverage: 1.0.2 dev: true @@ -896,7 +881,7 @@ packages: dependencies: '@babel/core': 7.23.2 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.20 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -918,10 +903,10 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.4 - '@types/istanbul-reports': 3.0.2 + '@types/istanbul-lib-coverage': 2.0.5 + '@types/istanbul-reports': 3.0.3 '@types/node': 20.5.7 - '@types/yargs': 17.0.28 + '@types/yargs': 17.0.29 chalk: 4.1.2 dev: true @@ -931,7 +916,7 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.20 dev: true /@jridgewell/resolve-uri@3.1.1: @@ -948,15 +933,15 @@ packages: resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} dependencies: '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.20 dev: true /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true - /@jridgewell/trace-mapping@0.3.19: - resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + /@jridgewell/trace-mapping@0.3.20: + resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 @@ -991,8 +976,8 @@ packages: - supports-color dev: false - /@mikro-orm/cli@5.8.9(@mikro-orm/migrations@5.8.9)(@mikro-orm/postgresql@5.8.9)(@mikro-orm/seeder@5.8.9)(pg@8.11.3): - resolution: {integrity: sha512-qt2/YRQJbNx6ldB1dbdA2IcydCqmSbIQSRVbJjvxM3fnv5pR7hQBg7rMuTzz0EbVSGEQS+heIpIKBTVEohlwsw==} + /@mikro-orm/cli@5.9.1(@mikro-orm/migrations@5.9.1)(@mikro-orm/postgresql@5.9.1)(@mikro-orm/seeder@5.9.1)(pg@8.11.3): + resolution: {integrity: sha512-nLjjEIH7aOww20VqH49bM22IK4868CfUnQNDXIcymhNUd8pNWOX3jos3xe4kEtoEVx8x5beq/1LEm8CAmhA6dA==} engines: {node: '>= 14.0.0'} hasBin: true peerDependencies: @@ -1029,11 +1014,11 @@ packages: optional: true dependencies: '@jercle/yargonaut': 1.1.5 - '@mikro-orm/core': 5.8.9(@mikro-orm/migrations@5.8.9)(@mikro-orm/postgresql@5.8.9)(@mikro-orm/seeder@5.8.9) - '@mikro-orm/knex': 5.8.9(@mikro-orm/core@5.8.9)(@mikro-orm/migrations@5.8.9)(pg@8.11.3) - '@mikro-orm/migrations': 5.8.9(@mikro-orm/core@5.8.9)(pg@8.11.3) - '@mikro-orm/postgresql': 5.8.9(@mikro-orm/core@5.8.9)(@mikro-orm/migrations@5.8.9)(@mikro-orm/seeder@5.8.9) - '@mikro-orm/seeder': 5.8.9(@mikro-orm/core@5.8.9) + '@mikro-orm/core': 5.9.1(@mikro-orm/migrations@5.9.1)(@mikro-orm/postgresql@5.9.1)(@mikro-orm/seeder@5.9.1) + '@mikro-orm/knex': 5.9.1(@mikro-orm/core@5.9.1)(@mikro-orm/migrations@5.9.1)(pg@8.11.3) + '@mikro-orm/migrations': 5.9.1(@mikro-orm/core@5.9.1)(pg@8.11.3) + '@mikro-orm/postgresql': 5.9.1(@mikro-orm/core@5.9.1)(@mikro-orm/migrations@5.9.1)(@mikro-orm/seeder@5.9.1) + '@mikro-orm/seeder': 5.9.1(@mikro-orm/core@5.9.1) fs-extra: 11.1.1 tsconfig-paths: 4.2.0 yargs: 17.7.2 @@ -1049,8 +1034,8 @@ packages: - tedious dev: false - /@mikro-orm/core@5.8.9(@mikro-orm/migrations@5.8.9)(@mikro-orm/postgresql@5.8.9)(@mikro-orm/seeder@5.8.9): - resolution: {integrity: sha512-fvc1dUs1Z/7Q/qf98dEwWKOHZ1caHkwJKBRV4ZSymfqJj5NyL3MK8Q8aURj/2PBepfrqdaiaInGJ0B7a1g6XJw==} + /@mikro-orm/core@5.9.1(@mikro-orm/migrations@5.9.1)(@mikro-orm/postgresql@5.9.1)(@mikro-orm/seeder@5.9.1): + resolution: {integrity: sha512-/74VuhbQTkvQJ5d+PZxop/xw/gc5BU64wljKDspTNqM+nEHPJV8MZvVE01jJM2sXs6lRRZDMYuh4CDypqT0MMg==} engines: {node: '>= 14.0.0'} peerDependencies: '@mikro-orm/better-sqlite': ^5.0.0 @@ -1085,20 +1070,20 @@ packages: '@mikro-orm/sqlite': optional: true dependencies: - '@mikro-orm/migrations': 5.8.9(@mikro-orm/core@5.8.9)(pg@8.11.3) - '@mikro-orm/postgresql': 5.8.9(@mikro-orm/core@5.8.9)(@mikro-orm/migrations@5.8.9)(@mikro-orm/seeder@5.8.9) - '@mikro-orm/seeder': 5.8.9(@mikro-orm/core@5.8.9) + '@mikro-orm/migrations': 5.9.1(@mikro-orm/core@5.9.1)(pg@8.11.3) + '@mikro-orm/postgresql': 5.9.1(@mikro-orm/core@5.9.1)(@mikro-orm/migrations@5.9.1)(@mikro-orm/seeder@5.9.1) + '@mikro-orm/seeder': 5.9.1(@mikro-orm/core@5.9.1) acorn-loose: 8.3.0 acorn-walk: 8.2.0 dotenv: 16.3.1 fs-extra: 11.1.1 globby: 11.1.0 - mikro-orm: 5.8.9 + mikro-orm: 5.9.1 reflect-metadata: 0.1.13 dev: false - /@mikro-orm/knex@5.8.9(@mikro-orm/core@5.8.9)(@mikro-orm/migrations@5.8.9)(pg@8.11.3): - resolution: {integrity: sha512-lEnSnaXQZ156ziB+xQlYGr3LJb3pIYGRm8sNcRO2AVQzTkOmXQENyeTc1cK+hzmNboByMlYescBZhe45+cQtwA==} + /@mikro-orm/knex@5.9.1(@mikro-orm/core@5.9.1)(@mikro-orm/migrations@5.9.1)(pg@8.11.3): + resolution: {integrity: sha512-Uuq2SkEOKgcXST14BvCMTp19YhpS9ZKxJpvAuPDQg/3sGjt3Y5nYwcKGOukChIwOREH1fveogKLbE1xLc7479g==} engines: {node: '>= 14.0.0'} peerDependencies: '@mikro-orm/core': ^5.0.0 @@ -1128,8 +1113,8 @@ packages: sqlite3: optional: true dependencies: - '@mikro-orm/core': 5.8.9(@mikro-orm/migrations@5.8.9)(@mikro-orm/postgresql@5.8.9)(@mikro-orm/seeder@5.8.9) - '@mikro-orm/migrations': 5.8.9(@mikro-orm/core@5.8.9)(pg@8.11.3) + '@mikro-orm/core': 5.9.1(@mikro-orm/migrations@5.9.1)(@mikro-orm/postgresql@5.9.1)(@mikro-orm/seeder@5.9.1) + '@mikro-orm/migrations': 5.9.1(@mikro-orm/core@5.9.1)(pg@8.11.3) fs-extra: 11.1.1 knex: 2.5.1(pg@8.11.3) pg: 8.11.3 @@ -1140,14 +1125,14 @@ packages: - tedious dev: false - /@mikro-orm/migrations@5.8.9(@mikro-orm/core@5.8.9)(pg@8.11.3): - resolution: {integrity: sha512-+TFJYyFQt2zuu0XsluZE/tNTIYty+xuqg7z1bBBv6DWIPDd50YmzZNC+MPplyudfT1pzjqUiqiwVzCV4ytfy7w==} + /@mikro-orm/migrations@5.9.1(@mikro-orm/core@5.9.1)(pg@8.11.3): + resolution: {integrity: sha512-AKqpvmSYqL3MV22sVVNvks5H/fuVaRar0qln1C+h8wrMfpe7zArFQSCgAKZX9Y69J2QUOQIxK+Hb9x23LFlGiw==} engines: {node: '>= 14.0.0'} peerDependencies: '@mikro-orm/core': ^5.0.0 dependencies: - '@mikro-orm/core': 5.8.9(@mikro-orm/migrations@5.8.9)(@mikro-orm/postgresql@5.8.9)(@mikro-orm/seeder@5.8.9) - '@mikro-orm/knex': 5.8.9(@mikro-orm/core@5.8.9)(@mikro-orm/migrations@5.8.9)(pg@8.11.3) + '@mikro-orm/core': 5.9.1(@mikro-orm/migrations@5.9.1)(@mikro-orm/postgresql@5.9.1)(@mikro-orm/seeder@5.9.1) + '@mikro-orm/knex': 5.9.1(@mikro-orm/core@5.9.1)(@mikro-orm/migrations@5.9.1)(pg@8.11.3) fs-extra: 11.1.1 knex: 2.5.1(pg@8.11.3) umzug: 3.3.1 @@ -1164,7 +1149,7 @@ packages: - tedious dev: false - /@mikro-orm/nestjs@5.2.2(@mikro-orm/core@5.8.9)(@nestjs/common@10.2.7)(@nestjs/core@10.2.7): + /@mikro-orm/nestjs@5.2.2(@mikro-orm/core@5.9.1)(@nestjs/common@10.2.8)(@nestjs/core@10.2.8): resolution: {integrity: sha512-NwPTmpmwf4/aX7FjKk/CTncYS7Mbr4fMvSOfbo9rOElySjpniTnk2cCGABgj2kaX9NSFHzWCUy5tAMIkoedd4A==} engines: {node: '>= 14.0.0'} peerDependencies: @@ -1172,13 +1157,13 @@ packages: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 dependencies: - '@mikro-orm/core': 5.8.9(@mikro-orm/migrations@5.8.9)(@mikro-orm/postgresql@5.8.9)(@mikro-orm/seeder@5.8.9) - '@nestjs/common': 10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@mikro-orm/core': 5.9.1(@mikro-orm/migrations@5.9.1)(@mikro-orm/postgresql@5.9.1)(@mikro-orm/seeder@5.9.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) dev: false - /@mikro-orm/postgresql@5.8.9(@mikro-orm/core@5.8.9)(@mikro-orm/migrations@5.8.9)(@mikro-orm/seeder@5.8.9): - resolution: {integrity: sha512-r8nIBXoVd5RCcSvst566UDrZVTrywdIcYmgIIYjhyXl68YjuiXsd6ncPIWiydX9mrxxBdKZ/uOKauzLW/qvOFw==} + /@mikro-orm/postgresql@5.9.1(@mikro-orm/core@5.9.1)(@mikro-orm/migrations@5.9.1)(@mikro-orm/seeder@5.9.1): + resolution: {integrity: sha512-ih7SUr3AYC8aq7jucFVYVAEge9U5q4gcDP7xapmKqRm5Pups9oC+qrXdpAyvzKqWazRfrydfLmLXKm+uqmTGHQ==} engines: {node: '>= 14.0.0'} peerDependencies: '@mikro-orm/core': ^5.0.0 @@ -1193,10 +1178,10 @@ packages: '@mikro-orm/seeder': optional: true dependencies: - '@mikro-orm/core': 5.8.9(@mikro-orm/migrations@5.8.9)(@mikro-orm/postgresql@5.8.9)(@mikro-orm/seeder@5.8.9) - '@mikro-orm/knex': 5.8.9(@mikro-orm/core@5.8.9)(@mikro-orm/migrations@5.8.9)(pg@8.11.3) - '@mikro-orm/migrations': 5.8.9(@mikro-orm/core@5.8.9)(pg@8.11.3) - '@mikro-orm/seeder': 5.8.9(@mikro-orm/core@5.8.9) + '@mikro-orm/core': 5.9.1(@mikro-orm/migrations@5.9.1)(@mikro-orm/postgresql@5.9.1)(@mikro-orm/seeder@5.9.1) + '@mikro-orm/knex': 5.9.1(@mikro-orm/core@5.9.1)(@mikro-orm/migrations@5.9.1)(pg@8.11.3) + '@mikro-orm/migrations': 5.9.1(@mikro-orm/core@5.9.1)(pg@8.11.3) + '@mikro-orm/seeder': 5.9.1(@mikro-orm/core@5.9.1) pg: 8.11.3 transitivePeerDependencies: - better-sqlite3 @@ -1209,25 +1194,25 @@ packages: - tedious dev: false - /@mikro-orm/reflection@5.8.9(@mikro-orm/core@5.8.9): - resolution: {integrity: sha512-aAOvldWjkcLn/eId5ar/PN8jWm0tKdyVMMCKZVkak0b0mDaHrKDPzzTFrAzhUsF8US9l5/t81E1ZSmjXG0vcxw==} + /@mikro-orm/reflection@5.9.1(@mikro-orm/core@5.9.1): + resolution: {integrity: sha512-v7IIOARblDiGBLkngds9iP8rrGCH/cuPvTJCv3Ovr5COYKMHualLT6wxYJ1I962Wy5g4lqCrrXLPJUV8GzLLoA==} engines: {node: '>= 14.0.0'} peerDependencies: '@mikro-orm/core': ^5.0.0 dependencies: - '@mikro-orm/core': 5.8.9(@mikro-orm/migrations@5.8.9)(@mikro-orm/postgresql@5.8.9)(@mikro-orm/seeder@5.8.9) + '@mikro-orm/core': 5.9.1(@mikro-orm/migrations@5.9.1)(@mikro-orm/postgresql@5.9.1)(@mikro-orm/seeder@5.9.1) globby: 11.1.0 ts-morph: 20.0.0 dev: false - /@mikro-orm/seeder@5.8.9(@mikro-orm/core@5.8.9): - resolution: {integrity: sha512-06z1NyLOx2RoBaenmQVpan6BZQanu+Nym0E1LMc9XZSiJPH9Z5gldVemhu1yhvgWtZ869ZyqWkxV2HqeBjE9zQ==} + /@mikro-orm/seeder@5.9.1(@mikro-orm/core@5.9.1): + resolution: {integrity: sha512-ZwxoE4nhsn+nbX6D670F9dBtT3Ja9f3myxO+YLdJ2MePej+4Q59viqgLacNbP3/vWqURSkKS2ZAD7KTi/73Bxg==} engines: {node: '>= 14.0.0'} peerDependencies: '@mikro-orm/core': ^5.0.0 dependencies: '@faker-js/faker': 7.6.0 - '@mikro-orm/core': 5.8.9(@mikro-orm/migrations@5.8.9)(@mikro-orm/postgresql@5.8.9)(@mikro-orm/seeder@5.8.9) + '@mikro-orm/core': 5.9.1(@mikro-orm/migrations@5.9.1)(@mikro-orm/postgresql@5.9.1)(@mikro-orm/seeder@5.9.1) fs-extra: 11.1.1 globby: 11.1.0 dev: false @@ -1239,9 +1224,9 @@ packages: ansi-colors: 4.1.3 dev: false - /@nestjs/cli@10.1.18: - resolution: {integrity: sha512-jQtG47keLsACt7b4YwJbTBYRm90n82gJpMaiR1HGAyQ9pccbctjSYu592eT4bxqkUWxPgBE3mpNynXj7dWAfrw==} - engines: {node: '>= 16'} + /@nestjs/cli@10.2.1: + resolution: {integrity: sha512-CAJAQwmxFZfB3RTvqz/eaXXWpyU+mZ4QSqfBYzjneTsPgF+uyOAW3yQpaLNn9Dfcv39R9UxSuAhayv6yuFd+Jg==} + engines: {node: '>= 16.14'} hasBin: true peerDependencies: '@swc/cli': ^0.1.62 @@ -1252,15 +1237,16 @@ packages: '@swc/core': optional: true dependencies: - '@angular-devkit/core': 16.2.3(chokidar@3.5.3) - '@angular-devkit/schematics': 16.2.3(chokidar@3.5.3) - '@angular-devkit/schematics-cli': 16.2.3(chokidar@3.5.3) - '@nestjs/schematics': 10.0.2(chokidar@3.5.3)(typescript@5.2.2) + '@angular-devkit/core': 16.2.8(chokidar@3.5.3) + '@angular-devkit/schematics': 16.2.8(chokidar@3.5.3) + '@angular-devkit/schematics-cli': 16.2.8(chokidar@3.5.3) + '@nestjs/schematics': 10.0.3(chokidar@3.5.3)(typescript@5.2.2) chalk: 4.1.2 chokidar: 3.5.3 cli-table3: 0.6.3 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.2.2)(webpack@5.88.2) + fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.2.2)(webpack@5.89.0) + glob: 10.3.10 inquirer: 8.2.6 node-emoji: 1.11.0 ora: 5.4.1 @@ -1272,7 +1258,7 @@ packages: tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.1.0 typescript: 5.2.2 - webpack: 5.88.2 + webpack: 5.89.0 webpack-node-externals: 3.0.0 transitivePeerDependencies: - esbuild @@ -1280,8 +1266,8 @@ packages: - webpack-cli dev: true - /@nestjs/common@10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-cUtCRXiUstDmh4bSBhVbq4cI439Gngp4LgLGLBmd5dqFQodfXKnSD441ldYfFiLz4rbUsnoMJz/8ZjuIEI+B7A==} + /@nestjs/common@10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-rmpwcdvq2IWMmsUVP8rsdKub6uDWk7dwCYo0aif50JTwcvcxzaP3iKVFKoSgvp0RKYu8h15+/AEOfaInmPpl0Q==} peerDependencies: class-transformer: '*' class-validator: '*' @@ -1293,6 +1279,7 @@ packages: class-validator: optional: true dependencies: + class-transformer: 0.5.1 class-validator: 0.14.0 iterare: 1.2.1 reflect-metadata: 0.1.13 @@ -1300,13 +1287,13 @@ packages: tslib: 2.6.2 uid: 2.0.2 - /@nestjs/config@3.1.1(@nestjs/common@10.2.7)(reflect-metadata@0.1.13): + /@nestjs/config@3.1.1(@nestjs/common@10.2.8)(reflect-metadata@0.1.13): resolution: {integrity: sha512-qu5QlNiJdqQtOsnB6lx4JCXPQ96jkKUsOGd+JXfXwqJqZcOSAq6heNFg0opW4pq4J/VZoNwoo87TNnx9wthnqQ==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 reflect-metadata: ^0.1.13 dependencies: - '@nestjs/common': 10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) dotenv: 16.3.1 dotenv-expand: 10.0.0 lodash: 4.17.21 @@ -1314,8 +1301,8 @@ packages: uuid: 9.0.0 dev: false - /@nestjs/core@10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-5GSu53QUUcwX17sNmlJPa1I0wIeAZOKbedyVuQx0ZAwWVa9g0wJBbsNP+R4EJ+j5Dkdzt/8xkiZvnKt8RFRR8g==} + /@nestjs/core@10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-9+MZ2s8ixfY9Bl/M9ofChiyYymcwdK9ZWNH4GDMF7Am7XRAQ1oqde6MYGG05rhQwiVXuTwaYLlXciJKfsrg5qg==} requiresBuild: true peerDependencies: '@nestjs/common': ^10.0.0 @@ -1332,8 +1319,8 @@ packages: '@nestjs/websockets': optional: true dependencies: - '@nestjs/common': 10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/platform-express': 10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/platform-express': 10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -1345,17 +1332,17 @@ packages: transitivePeerDependencies: - encoding - /@nestjs/jwt@10.1.1(@nestjs/common@10.2.7): + /@nestjs/jwt@10.1.1(@nestjs/common@10.2.8): resolution: {integrity: sha512-sISYylg8y1Mb7saxPx5Zh11i7v9JOh70CEC/rN6g43MrbFlJ57c1eYFrffxip1YAx3DmV4K67yXob3syKZMOew==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 dependencies: - '@nestjs/common': 10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@types/jsonwebtoken': 9.0.2 jsonwebtoken: 9.0.0 dev: false - /@nestjs/mapped-types@2.0.2(@nestjs/common@10.2.7)(class-validator@0.14.0)(reflect-metadata@0.1.13): + /@nestjs/mapped-types@2.0.2(@nestjs/common@10.2.8)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13): resolution: {integrity: sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -1368,29 +1355,30 @@ packages: class-validator: optional: true dependencies: - '@nestjs/common': 10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + class-transformer: 0.5.1 class-validator: 0.14.0 reflect-metadata: 0.1.13 dev: false - /@nestjs/passport@10.0.2(@nestjs/common@10.2.7)(passport@0.6.0): + /@nestjs/passport@10.0.2(@nestjs/common@10.2.8)(passport@0.6.0): resolution: {integrity: sha512-od31vfB2z3y05IDB5dWSbCGE2+pAf2k2WCBinNuTTOxN0O0+wtO1L3kawj/aCW3YR9uxsTOVbTDwtwgpNNsnjQ==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 passport: ^0.4.0 || ^0.5.0 || ^0.6.0 dependencies: - '@nestjs/common': 10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) passport: 0.6.0 dev: false - /@nestjs/platform-express@10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7): - resolution: {integrity: sha512-p+kp6aJtkgAdVpUrCVmM6MKtOvjsbt7QofBiZMidjYesZkMeG5gZ1D2SK8XzvQ8VXHJfFgEdY2xcKGB+wJLOYQ==} + /@nestjs/platform-express@10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8): + resolution: {integrity: sha512-WoSSVtwIRc5AdGMHWVzWZK4JZLT0f4o2xW8P9gQvcX+omL8W1kXCfY8GQYXNBG84XmBNYH8r0FtC8oMe/lH5NQ==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 dependencies: - '@nestjs/common': 10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) body-parser: 1.20.2 cors: 2.8.5 express: 4.18.2 @@ -1399,27 +1387,27 @@ packages: transitivePeerDependencies: - supports-color - /@nestjs/schedule@3.0.4(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(reflect-metadata@0.1.13): + /@nestjs/schedule@3.0.4(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(reflect-metadata@0.1.13): resolution: {integrity: sha512-uFJpuZsXfpvgx2y7/KrIZW9e1L68TLiwRodZ6+Gc8xqQiHSUzAVn+9F4YMxWFlHITZvvkjWziUFgRNCitDcTZQ==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 reflect-metadata: ^0.1.12 dependencies: - '@nestjs/common': 10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) cron: 2.4.3 reflect-metadata: 0.1.13 uuid: 9.0.1 dev: false - /@nestjs/schematics@10.0.2(chokidar@3.5.3)(typescript@5.2.2): - resolution: {integrity: sha512-DaZZjymYoIfRqC5W62lnYXIIods1PDY6CGc8+IpRwyinzffjKxZ3DF3exu+mdyvllzkXo9DTXkoX4zOPSJHCkw==} + /@nestjs/schematics@10.0.3(chokidar@3.5.3)(typescript@5.2.2): + resolution: {integrity: sha512-2BRujK0GqGQ7j1Zpz+obVfskDnnOeVKt5aXoSaVngKo8Oczy8uYCY+R547TQB+Kf35epdfFER2pVnQrX3/It5A==} peerDependencies: typescript: '>=4.8.2' dependencies: - '@angular-devkit/core': 16.1.8(chokidar@3.5.3) - '@angular-devkit/schematics': 16.1.8(chokidar@3.5.3) + '@angular-devkit/core': 16.2.8(chokidar@3.5.3) + '@angular-devkit/schematics': 16.2.8(chokidar@3.5.3) comment-json: 4.2.3 jsonc-parser: 3.2.0 pluralize: 8.0.0 @@ -1428,8 +1416,8 @@ packages: - chokidar dev: true - /@nestjs/swagger@7.1.13(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(class-validator@0.14.0)(reflect-metadata@0.1.13): - resolution: {integrity: sha512-aHfW0rDZZKTuPVSkxutBCB16lBy5vrsHVoRF5RvPtH7U2cm4Vf+OnfhxKKuG2g2Xocn9sDL+JAyVlY2VN3ytTw==} + /@nestjs/swagger@7.1.14(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-2Ol4S6qHeYVVmkshkWBM8E/qkmEqEOUj2QIewr0jLSyo30H7f3v81pJyks6pTLy4PK0LGUXojMvIfFIE3mmGQQ==} peerDependencies: '@fastify/static': ^6.0.0 '@nestjs/common': ^9.0.0 || ^10.0.0 @@ -1445,9 +1433,10 @@ packages: class-validator: optional: true dependencies: - '@nestjs/common': 10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/mapped-types': 2.0.2(@nestjs/common@10.2.7)(class-validator@0.14.0)(reflect-metadata@0.1.13) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/mapped-types': 2.0.2(@nestjs/common@10.2.8)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) + class-transformer: 0.5.1 class-validator: 0.14.0 js-yaml: 4.1.0 lodash: 4.17.21 @@ -1456,8 +1445,8 @@ packages: swagger-ui-dist: 5.9.0 dev: false - /@nestjs/testing@10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(@nestjs/platform-express@10.2.7): - resolution: {integrity: sha512-d2SIqiJIf/7NSILeNNWSdRvTTpHSouGgisGHwf5PVDC7z4/yXZw/wPO9eJhegnxFlqk6n2LW4QBTmMzbqjAfHA==} + /@nestjs/testing@10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(@nestjs/platform-express@10.2.8): + resolution: {integrity: sha512-9Kj5IQhM67/nj/MT6Wi2OmWr5YQnCMptwKVFrX1TDaikpY12196v7frk0jVjdT7wms7rV07GZle9I2z0aSjqtQ==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 @@ -1469,9 +1458,9 @@ packages: '@nestjs/platform-express': optional: true dependencies: - '@nestjs/common': 10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/platform-express': 10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/platform-express': 10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8) tslib: 2.6.2 dev: true @@ -1504,8 +1493,15 @@ packages: transitivePeerDependencies: - encoding - /@rushstack/ts-command-line@4.16.1: - resolution: {integrity: sha512-+OCsD553GYVLEmz12yiFjMOzuPeCiZ3f8wTiFHL30ZVXexTyPmgjwXEhg2K2P0a2lVf+8YBy7WtPoflB2Fp8/A==} + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@rushstack/ts-command-line@4.17.1: + resolution: {integrity: sha512-2jweO1O57BYP5qdBGl6apJLB+aRIn5ccIRTPDyULh0KMwVzFqWtw6IZWt1qtUoZD/pD2RNkIOosH6Cq45rIYeg==} dependencies: '@types/argparse': 1.0.38 argparse: 1.0.10 @@ -1562,118 +1558,118 @@ packages: resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} dev: false - /@types/babel__core@7.20.2: - resolution: {integrity: sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==} + /@types/babel__core@7.20.3: + resolution: {integrity: sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==} dependencies: '@babel/parser': 7.23.0 '@babel/types': 7.23.0 - '@types/babel__generator': 7.6.5 - '@types/babel__template': 7.4.2 - '@types/babel__traverse': 7.20.2 + '@types/babel__generator': 7.6.6 + '@types/babel__template': 7.4.3 + '@types/babel__traverse': 7.20.3 dev: true - /@types/babel__generator@7.6.5: - resolution: {integrity: sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==} + /@types/babel__generator@7.6.6: + resolution: {integrity: sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==} dependencies: '@babel/types': 7.23.0 dev: true - /@types/babel__template@7.4.2: - resolution: {integrity: sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==} + /@types/babel__template@7.4.3: + resolution: {integrity: sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==} dependencies: '@babel/parser': 7.23.0 '@babel/types': 7.23.0 dev: true - /@types/babel__traverse@7.20.2: - resolution: {integrity: sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==} + /@types/babel__traverse@7.20.3: + resolution: {integrity: sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==} dependencies: '@babel/types': 7.23.0 dev: true - /@types/bcrypt@5.0.0: - resolution: {integrity: sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==} + /@types/bcrypt@5.0.1: + resolution: {integrity: sha512-dIIrEsLV1/v0AUNI8oHMaRRTSeVjoy5ID8oclJavtPj8CwPJoD1eFoNXEypuu6k091brEzBeOo3LlxeAH9zRZg==} dependencies: '@types/node': 20.5.7 dev: true - /@types/body-parser@1.19.3: - resolution: {integrity: sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==} + /@types/body-parser@1.19.4: + resolution: {integrity: sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==} dependencies: - '@types/connect': 3.4.36 + '@types/connect': 3.4.37 '@types/node': 20.5.7 dev: true - /@types/connect@3.4.36: - resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} + /@types/connect@3.4.37: + resolution: {integrity: sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==} dependencies: '@types/node': 20.5.7 dev: true - /@types/cookiejar@2.1.2: - resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==} + /@types/cookiejar@2.1.3: + resolution: {integrity: sha512-LZ8SD3LpNmLMDLkG2oCBjZg+ETnx6XdCjydUE0HwojDmnDfDUnhMKKbtth1TZh+hzcqb03azrYWoXLS8sMXdqg==} dev: true - /@types/eslint-scope@3.7.5: - resolution: {integrity: sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==} + /@types/eslint-scope@3.7.6: + resolution: {integrity: sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==} dependencies: - '@types/eslint': 8.44.4 - '@types/estree': 1.0.2 + '@types/eslint': 8.44.6 + '@types/estree': 1.0.4 dev: true - /@types/eslint@8.44.4: - resolution: {integrity: sha512-lOzjyfY/D9QR4hY9oblZ76B90MYTB3RrQ4z2vBIJKj9ROCRqdkYl2gSUx1x1a4IWPjKJZLL4Aw1Zfay7eMnmnA==} + /@types/eslint@8.44.6: + resolution: {integrity: sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==} dependencies: - '@types/estree': 1.0.2 - '@types/json-schema': 7.0.13 + '@types/estree': 1.0.4 + '@types/json-schema': 7.0.14 dev: true - /@types/estree@1.0.2: - resolution: {integrity: sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==} + /@types/estree@1.0.4: + resolution: {integrity: sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==} dev: true - /@types/express-serve-static-core@4.17.37: - resolution: {integrity: sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==} + /@types/express-serve-static-core@4.17.39: + resolution: {integrity: sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ==} dependencies: '@types/node': 20.5.7 - '@types/qs': 6.9.8 - '@types/range-parser': 1.2.5 - '@types/send': 0.17.2 + '@types/qs': 6.9.9 + '@types/range-parser': 1.2.6 + '@types/send': 0.17.3 dev: true - /@types/express@4.17.19: - resolution: {integrity: sha512-UtOfBtzN9OvpZPPbnnYunfjM7XCI4jyk1NvnFhTVz5krYAnW4o5DCoIekvms+8ApqhB4+9wSge1kBijdfTSmfg==} + /@types/express@4.17.20: + resolution: {integrity: sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==} dependencies: - '@types/body-parser': 1.19.3 - '@types/express-serve-static-core': 4.17.37 - '@types/qs': 6.9.8 - '@types/serve-static': 1.15.3 + '@types/body-parser': 1.19.4 + '@types/express-serve-static-core': 4.17.39 + '@types/qs': 6.9.9 + '@types/serve-static': 1.15.4 dev: true - /@types/graceful-fs@4.1.7: - resolution: {integrity: sha512-MhzcwU8aUygZroVwL2jeYk6JisJrPl/oov/gsgGCue9mkgl9wjGbzReYQClxiUgFDnib9FuHqTndccKeZKxTRw==} + /@types/graceful-fs@4.1.8: + resolution: {integrity: sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw==} dependencies: '@types/node': 20.5.7 dev: true - /@types/http-errors@2.0.2: - resolution: {integrity: sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==} + /@types/http-errors@2.0.3: + resolution: {integrity: sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==} dev: true - /@types/istanbul-lib-coverage@2.0.4: - resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + /@types/istanbul-lib-coverage@2.0.5: + resolution: {integrity: sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==} dev: true - /@types/istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-gPQuzaPR5h/djlAv2apEG1HVOyj1IUs7GpfMZixU0/0KXT3pm64ylHuMUI1/Akh+sq/iikxg6Z2j+fcMDXaaTQ==} + /@types/istanbul-lib-report@3.0.2: + resolution: {integrity: sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==} dependencies: - '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-lib-coverage': 2.0.5 dev: true - /@types/istanbul-reports@3.0.2: - resolution: {integrity: sha512-kv43F9eb3Lhj+lr/Hn6OcLCs/sSM8bt+fIaP11rCYngfV6NVjzWXJ17owQtDQTL9tQ8WSLUrGsSJ6rJz0F1w1A==} + /@types/istanbul-reports@3.0.3: + resolution: {integrity: sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==} dependencies: - '@types/istanbul-lib-report': 3.0.1 + '@types/istanbul-lib-report': 3.0.2 dev: true /@types/jest@29.5.2: @@ -1683,8 +1679,8 @@ packages: pretty-format: 29.7.0 dev: true - /@types/json-schema@7.0.13: - resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==} + /@types/json-schema@7.0.14: + resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==} dev: true /@types/json5@0.0.29: @@ -1697,123 +1693,119 @@ packages: '@types/node': 20.5.7 dev: false - /@types/jsonwebtoken@9.0.3: - resolution: {integrity: sha512-b0jGiOgHtZ2jqdPgPnP6WLCXZk1T8p06A/vPGzUvxpFGgKMbjXJDjC5m52ErqBnIuWZFgGoIJyRdeG5AyreJjA==} + /@types/jsonwebtoken@9.0.4: + resolution: {integrity: sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==} dependencies: '@types/node': 20.5.7 dev: true - /@types/luxon@3.3.2: - resolution: {integrity: sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==} + /@types/luxon@3.3.3: + resolution: {integrity: sha512-/BJF3NT0pRMuxrenr42emRUF67sXwcZCd+S1ksG/Fcf9O7C3kKCY4uJSbKBE4KDUIYr3WMsvfmWD8hRjXExBJQ==} dev: false - /@types/mime@1.3.3: - resolution: {integrity: sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==} + /@types/mime@1.3.4: + resolution: {integrity: sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==} dev: true - /@types/mime@3.0.2: - resolution: {integrity: sha512-Wj+fqpTLtTbG7c0tH47dkahefpLKEbB+xAZuLq7b4/IDHPl/n6VoXcyUQ2bypFlbSwvCr0y+bD4euTTqTJsPxQ==} + /@types/mime@3.0.3: + resolution: {integrity: sha512-i8MBln35l856k5iOhKk2XJ4SeAWg75mLIpZB4v6imOagKL6twsukBZGDMNhdOVk7yRFTMPpfILocMos59Q1otQ==} dev: true - /@types/multer@1.4.8: - resolution: {integrity: sha512-VMZOW6mnmMMhA5m3fsCdXBwFwC+a+27/8gctNMuQC4f7UtWcF79KAFGoIfKZ4iqrElgWIa3j5vhMJDp0iikQ1g==} + /@types/multer@1.4.9: + resolution: {integrity: sha512-9NSvPJ2E8bNTc8XtJq1Cimx2Wrn2Ah48F15B2Du/hM8a8CHLhVbJMlF3ZCqhvMdht7Sa+YdP0aKP7N4fxDcrrg==} dependencies: - '@types/express': 4.17.19 + '@types/express': 4.17.20 dev: true /@types/node@20.5.7: resolution: {integrity: sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==} - /@types/nodemailer@6.4.11: - resolution: {integrity: sha512-Ld2c0frwpGT4VseuoeboCXQ7UJIkK3X7Lx/4YsZEiUHtHsthWAOCYtf6PAiLhMtfwV0cWJRabLBS3+LD8x6Nrw==} + /@types/nodemailer@6.4.13: + resolution: {integrity: sha512-889Vq/77eEpidCwh52sVWpbnqQmIwL8yVBekNbrztVEaWKOCRH3Eq6hjIJh1jwsGDEAJEH0RR+YhpH9mfELLKA==} dependencies: '@types/node': 20.5.7 dev: true - /@types/parse-json@4.0.0: - resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} - dev: true - - /@types/passport-jwt@3.0.10: - resolution: {integrity: sha512-D2A911g2uiFsq/XXFBxQjcBcK4c6zPF2gAx9blEfz2AOXx5UUwsd8ZcMTcPe8z9dhW8LQBYLjv+vug2dvnRevA==} + /@types/passport-jwt@3.0.12: + resolution: {integrity: sha512-nXCd1lu20rw//nZ5AnK1FnlVZdSC4R5xksquev9oAJlXwJw0irMdZ7dRAE4KDlalptKObiaoam6BQ8lpujeZog==} dependencies: - '@types/express': 4.17.19 - '@types/jsonwebtoken': 9.0.3 - '@types/passport-strategy': 0.2.36 + '@types/express': 4.17.20 + '@types/jsonwebtoken': 9.0.4 + '@types/passport-strategy': 0.2.37 dev: true - /@types/passport-strategy@0.2.36: - resolution: {integrity: sha512-hotVZuaCt04LJYXfZD5B+5UeCcRVG8IjKaLLGTJ1eFp0wiFQA2XfsqslGGInWje+OysNNLPH/ducce5GXHDC1Q==} + /@types/passport-strategy@0.2.37: + resolution: {integrity: sha512-ltgwLnwHVfpjK7/66lpv43hiz90nIVb36JmeB0iF3FAZoHX6+LbkY5Ey97Bm8Jr0uGhQyDFEsSOOfejp5PJehg==} dependencies: - '@types/express': 4.17.19 - '@types/passport': 1.0.13 + '@types/express': 4.17.20 + '@types/passport': 1.0.14 dev: true - /@types/passport@1.0.13: - resolution: {integrity: sha512-XXURryL+EZAWtbQFOHX1eNB+RJwz5XMPPz1xrGpEKr2xUZCXM4NCPkHMtZQ3B2tTSG/1IRaAcTHjczRA4sSFCw==} + /@types/passport@1.0.14: + resolution: {integrity: sha512-D6p2ygR2S7Cq5PO7iUaEIQu/5WrM0tONu6Lxgk0C9r3lafQIlVpWCo3V/KI9To3OqHBxcfQaOeK+8AvwW5RYmw==} dependencies: - '@types/express': 4.17.19 + '@types/express': 4.17.20 dev: true - /@types/qs@6.9.8: - resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==} + /@types/qs@6.9.9: + resolution: {integrity: sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==} dev: true - /@types/range-parser@1.2.5: - resolution: {integrity: sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==} + /@types/range-parser@1.2.6: + resolution: {integrity: sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==} dev: true - /@types/semver@7.5.3: - resolution: {integrity: sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==} + /@types/semver@7.5.4: + resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==} dev: true - /@types/send@0.17.2: - resolution: {integrity: sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==} + /@types/send@0.17.3: + resolution: {integrity: sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==} dependencies: - '@types/mime': 1.3.3 + '@types/mime': 1.3.4 '@types/node': 20.5.7 dev: true - /@types/serve-static@1.15.3: - resolution: {integrity: sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==} + /@types/serve-static@1.15.4: + resolution: {integrity: sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==} dependencies: - '@types/http-errors': 2.0.2 - '@types/mime': 3.0.2 + '@types/http-errors': 2.0.3 + '@types/mime': 3.0.3 '@types/node': 20.5.7 dev: true - /@types/stack-utils@2.0.1: - resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} + /@types/stack-utils@2.0.2: + resolution: {integrity: sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==} dev: true - /@types/superagent@4.1.19: - resolution: {integrity: sha512-McM1mlc7PBZpCaw0fw/36uFqo0YeA6m8JqoyE4OfqXsZCIg0hPP2xdE6FM7r6fdprDZHlJwDpydUj1R++93hCA==} + /@types/superagent@4.1.20: + resolution: {integrity: sha512-GfpwJgYSr3yO+nArFkmyqv3i0vZavyEG5xPd/o95RwpKYpsOKJYI5XLdxLpdRbZI3YiGKKdIOFIf/jlP7A0Jxg==} dependencies: - '@types/cookiejar': 2.1.2 + '@types/cookiejar': 2.1.3 '@types/node': 20.5.7 dev: true - /@types/supertest@2.0.14: - resolution: {integrity: sha512-Q900DeeHNFF3ZYYepf/EyJfZDA2JrnWLaSQ0YNV7+2GTo8IlJzauEnDGhya+hauncpBYTYGpVHwGdssJeAQ7eA==} + /@types/supertest@2.0.15: + resolution: {integrity: sha512-jUCZZ/TMcpGzoSaed9Gjr8HCf3HehExdibyw3OHHEL1als1KmyzcOZZH4MjbObI8TkWsEr7bc7gsW0WTDni+qQ==} dependencies: - '@types/superagent': 4.1.19 + '@types/superagent': 4.1.20 dev: true - /@types/validator@13.11.3: - resolution: {integrity: sha512-jxjhh33aTYDHnrV1vZ3AvWQHfrGx2f5UxKjaP13l5q04fG+/hCKKm0MfodIoCqxevhbcfBb6ZjynyHuQ/jueGQ==} + /@types/validator@13.11.5: + resolution: {integrity: sha512-xW4qsT4UIYILu+7ZrBnfQdBYniZrMLYYK3wN9M/NdeIHgBN5pZI2/8Q7UfdWIcr5RLJv/OGENsx91JIpUUoC7Q==} - /@types/yargs-parser@21.0.1: - resolution: {integrity: sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ==} + /@types/yargs-parser@21.0.2: + resolution: {integrity: sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==} dev: true - /@types/yargs@17.0.28: - resolution: {integrity: sha512-N3e3fkS86hNhtk6BEnc0rj3zcehaxx8QWhCROJkqpl5Zaoi7nAic3jH8q94jVD3zu5LGk+PUB6KAiDmimYOEQw==} + /@types/yargs@17.0.29: + resolution: {integrity: sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==} dependencies: - '@types/yargs-parser': 21.0.1 + '@types/yargs-parser': 21.0.2 dev: true - /@typescript-eslint/eslint-plugin@6.8.0(@typescript-eslint/parser@6.8.0)(eslint@8.51.0)(typescript@5.2.2): - resolution: {integrity: sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==} + /@typescript-eslint/eslint-plugin@6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.52.0)(typescript@5.2.2): + resolution: {integrity: sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha @@ -1823,14 +1815,14 @@ packages: typescript: optional: true dependencies: - '@eslint-community/regexpp': 4.9.1 - '@typescript-eslint/parser': 6.8.0(eslint@8.51.0)(typescript@5.2.2) - '@typescript-eslint/scope-manager': 6.8.0 - '@typescript-eslint/type-utils': 6.8.0(eslint@8.51.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.8.0(eslint@8.51.0)(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.8.0 + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.9.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.9.1 + '@typescript-eslint/type-utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.9.1 debug: 4.3.4 - eslint: 8.51.0 + eslint: 8.52.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare: 1.4.0 @@ -1841,8 +1833,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.8.0(eslint@8.51.0)(typescript@5.2.2): - resolution: {integrity: sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==} + /@typescript-eslint/parser@6.9.1(eslint@8.52.0)(typescript@5.2.2): + resolution: {integrity: sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -1851,27 +1843,27 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.8.0 - '@typescript-eslint/types': 6.8.0 - '@typescript-eslint/typescript-estree': 6.8.0(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.8.0 + '@typescript-eslint/scope-manager': 6.9.1 + '@typescript-eslint/types': 6.9.1 + '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.9.1 debug: 4.3.4 - eslint: 8.51.0 + eslint: 8.52.0 typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/scope-manager@6.8.0: - resolution: {integrity: sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==} + /@typescript-eslint/scope-manager@6.9.1: + resolution: {integrity: sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.8.0 - '@typescript-eslint/visitor-keys': 6.8.0 + '@typescript-eslint/types': 6.9.1 + '@typescript-eslint/visitor-keys': 6.9.1 dev: true - /@typescript-eslint/type-utils@6.8.0(eslint@8.51.0)(typescript@5.2.2): - resolution: {integrity: sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==} + /@typescript-eslint/type-utils@6.9.1(eslint@8.52.0)(typescript@5.2.2): + resolution: {integrity: sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -1880,23 +1872,23 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.8.0(typescript@5.2.2) - '@typescript-eslint/utils': 6.8.0(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) + '@typescript-eslint/utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) debug: 4.3.4 - eslint: 8.51.0 + eslint: 8.52.0 ts-api-utils: 1.0.3(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/types@6.8.0: - resolution: {integrity: sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==} + /@typescript-eslint/types@6.9.1: + resolution: {integrity: sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@6.8.0(typescript@5.2.2): - resolution: {integrity: sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==} + /@typescript-eslint/typescript-estree@6.9.1(typescript@5.2.2): + resolution: {integrity: sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -1904,8 +1896,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.8.0 - '@typescript-eslint/visitor-keys': 6.8.0 + '@typescript-eslint/types': 6.9.1 + '@typescript-eslint/visitor-keys': 6.9.1 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 @@ -1916,33 +1908,37 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.8.0(eslint@8.51.0)(typescript@5.2.2): - resolution: {integrity: sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==} + /@typescript-eslint/utils@6.9.1(eslint@8.52.0)(typescript@5.2.2): + resolution: {integrity: sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.51.0) - '@types/json-schema': 7.0.13 - '@types/semver': 7.5.3 - '@typescript-eslint/scope-manager': 6.8.0 - '@typescript-eslint/types': 6.8.0 - '@typescript-eslint/typescript-estree': 6.8.0(typescript@5.2.2) - eslint: 8.51.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@types/json-schema': 7.0.14 + '@types/semver': 7.5.4 + '@typescript-eslint/scope-manager': 6.9.1 + '@typescript-eslint/types': 6.9.1 + '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) + eslint: 8.52.0 semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/visitor-keys@6.8.0: - resolution: {integrity: sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==} + /@typescript-eslint/visitor-keys@6.9.1: + resolution: {integrity: sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.8.0 + '@typescript-eslint/types': 6.9.1 eslint-visitor-keys: 3.4.3 dev: true + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + /@webassemblyjs/ast@1.11.6: resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==} dependencies: @@ -2072,35 +2068,41 @@ packages: mime-types: 2.1.35 negotiator: 0.6.3 - /acorn-import-assertions@1.9.0(acorn@8.10.0): + /acorn-import-assertions@1.9.0(acorn@8.11.2): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} peerDependencies: acorn: ^8 dependencies: - acorn: 8.10.0 + acorn: 8.11.2 dev: true - /acorn-jsx@5.3.2(acorn@8.10.0): + /acorn-jsx@5.3.2(acorn@8.11.2): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.10.0 + acorn: 8.11.2 dev: true /acorn-loose@8.3.0: resolution: {integrity: sha512-75lAs9H19ldmW+fAbyqHdjgdCrz0pWGXKmnqFoh8PyVd1L2RIb4RzYrSjmopeqv3E1G3/Pimu6GgLlrGbrkF7w==} engines: {node: '>=0.4.0'} dependencies: - acorn: 8.10.0 + acorn: 8.11.2 dev: false /acorn-walk@8.2.0: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} + dev: false - /acorn@8.10.0: - resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + /acorn-walk@8.3.0: + resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.11.2: + resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} engines: {node: '>=0.4.0'} hasBin: true @@ -2165,6 +2167,11 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -2183,6 +2190,11 @@ packages: engines: {node: '>=10'} dev: true + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -2220,7 +2232,7 @@ packages: /array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 is-array-buffer: 3.0.2 dev: true @@ -2231,10 +2243,10 @@ packages: resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 - es-abstract: 1.22.2 - get-intrinsic: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 is-string: 1.0.7 dev: true @@ -2250,31 +2262,31 @@ packages: resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 - es-abstract: 1.22.2 - es-shim-unscopables: 1.0.0 - get-intrinsic: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + get-intrinsic: 1.2.2 dev: true /array.prototype.flat@1.3.2: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 - es-abstract: 1.22.2 - es-shim-unscopables: 1.0.0 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 dev: true /array.prototype.flatmap@1.3.2: resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 - es-abstract: 1.22.2 - es-shim-unscopables: 1.0.0 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 dev: true /arraybuffer.prototype.slice@1.0.2: @@ -2282,10 +2294,10 @@ packages: engines: {node: '>= 0.4'} dependencies: array-buffer-byte-length: 1.0.0 - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 - es-abstract: 1.22.2 - get-intrinsic: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 is-array-buffer: 3.0.2 is-shared-array-buffer: 1.0.2 dev: true @@ -2315,7 +2327,7 @@ packages: dependencies: '@babel/core': 7.23.2 '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.2 + '@types/babel__core': 7.20.3 babel-plugin-istanbul: 6.1.1 babel-preset-jest: 29.6.3(@babel/core@7.23.2) chalk: 4.1.2 @@ -2344,8 +2356,8 @@ packages: dependencies: '@babel/template': 7.22.15 '@babel/types': 7.23.0 - '@types/babel__core': 7.20.2 - '@types/babel__traverse': 7.20.2 + '@types/babel__core': 7.20.3 + '@types/babel__traverse': 7.20.3 dev: true /babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.2): @@ -2468,8 +2480,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001549 - electron-to-chromium: 1.4.556 + caniuse-lite: 1.0.30001559 + electron-to-chromium: 1.4.574 node-releases: 2.0.13 update-browserslist-db: 1.0.13(browserslist@4.22.1) dev: true @@ -2515,11 +2527,12 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - /call-bind@1.0.2: - resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} dependencies: function-bind: 1.1.2 - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -2536,8 +2549,8 @@ packages: engines: {node: '>=10'} dev: true - /caniuse-lite@1.0.30001549: - resolution: {integrity: sha512-qRp48dPYSCYaP+KurZLhDYdVE+yEyht/3NlmcJgVQ2VMGt6JL36ndQ/7rgspdZsJuxDPFIo/OzBT2+GmIJ53BA==} + /caniuse-lite@1.0.30001559: + resolution: {integrity: sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==} dev: true /chalk@2.4.2: @@ -2602,11 +2615,14 @@ packages: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} dev: true + /class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + /class-validator@0.14.0: resolution: {integrity: sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==} dependencies: - '@types/validator': 13.11.3 - libphonenumber-js: 1.10.48 + '@types/validator': 13.11.5 + libphonenumber-js: 1.10.49 validator: 13.11.0 /cli-cursor@3.1.0: @@ -2799,15 +2815,20 @@ packages: object-assign: 4.1.1 vary: 1.1.2 - /cosmiconfig@7.1.0: - resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} - engines: {node: '>=10'} + /cosmiconfig@8.3.6(typescript@5.2.2): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true dependencies: - '@types/parse-json': 4.0.0 import-fresh: 3.3.0 + js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 - yaml: 1.10.2 + typescript: 5.2.2 dev: true /create-jest@29.7.0(@types/node@20.5.7)(ts-node@10.9.1): @@ -2836,7 +2857,7 @@ packages: /cron@2.4.3: resolution: {integrity: sha512-YBvExkQYF7w0PxyeFLRyr817YVDhGxaCi5/uRRMqa4aWD3IFKRd+uNbpW1VWMdqQy8PZ7CElc+accXJcauPKzQ==} dependencies: - '@types/luxon': 3.3.2 + '@types/luxon': 3.3.3 luxon: 3.3.0 dev: false @@ -2921,17 +2942,16 @@ packages: resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.2 gopd: 1.0.1 - has-property-descriptors: 1.0.0 - dev: true + has-property-descriptors: 1.0.1 /define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} dependencies: define-data-property: 1.1.1 - has-property-descriptors: 1.0.0 + has-property-descriptors: 1.0.1 object-keys: 1.1.1 dev: true @@ -3009,6 +3029,10 @@ packages: engines: {node: '>=12'} dev: false + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} dependencies: @@ -3018,8 +3042,8 @@ packages: /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - /electron-to-chromium@1.4.556: - resolution: {integrity: sha512-6RPN0hHfzDU8D56E72YkDvnLw5Cj2NMXZGg3UkgyoHxjVhG99KZpsKgBWMmTy0Ei89xwan+rbRsVB9yzATmYzQ==} + /electron-to-chromium@1.4.574: + resolution: {integrity: sha512-bg1m8L0n02xRzx4LsTTMbBPiUd9yIR+74iPtS/Ao65CuXvhVZHP0ym1kSdDG3yHFDXqHQQBKujlN1AQ8qZnyFg==} dev: true /emittery@0.13.1: @@ -3029,6 +3053,10 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + /encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -3052,26 +3080,26 @@ packages: is-arrayish: 0.2.1 dev: true - /es-abstract@1.22.2: - resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} + /es-abstract@1.22.3: + resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} engines: {node: '>= 0.4'} dependencies: array-buffer-byte-length: 1.0.0 arraybuffer.prototype.slice: 1.0.2 available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - es-set-tostringtag: 2.0.1 + call-bind: 1.0.5 + es-set-tostringtag: 2.0.2 es-to-primitive: 1.2.1 function.prototype.name: 1.1.6 - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.2 get-symbol-description: 1.0.0 globalthis: 1.0.3 gopd: 1.0.1 - has: 1.0.4 - has-property-descriptors: 1.0.0 + has-property-descriptors: 1.0.1 has-proto: 1.0.1 has-symbols: 1.0.3 - internal-slot: 1.0.5 + hasown: 2.0.0 + internal-slot: 1.0.6 is-array-buffer: 3.0.2 is-callable: 1.2.7 is-negative-zero: 2.0.2 @@ -3080,7 +3108,7 @@ packages: is-string: 1.0.7 is-typed-array: 1.1.12 is-weakref: 1.0.2 - object-inspect: 1.13.0 + object-inspect: 1.13.1 object-keys: 1.1.1 object.assign: 4.1.4 regexp.prototype.flags: 1.5.1 @@ -3094,26 +3122,26 @@ packages: typed-array-byte-offset: 1.0.0 typed-array-length: 1.0.4 unbox-primitive: 1.0.2 - which-typed-array: 1.1.11 + which-typed-array: 1.1.13 dev: true /es-module-lexer@1.3.1: resolution: {integrity: sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==} dev: true - /es-set-tostringtag@2.0.1: - resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + /es-set-tostringtag@2.0.2: + resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.2.1 - has: 1.0.4 + get-intrinsic: 1.2.2 has-tostringtag: 1.0.0 + hasown: 2.0.0 dev: true - /es-shim-unscopables@1.0.0: - resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + /es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} dependencies: - has: 1.0.4 + hasown: 2.0.0 dev: true /es-to-primitive@1.2.1: @@ -3147,26 +3175,26 @@ packages: engines: {node: '>=10'} dev: true - /eslint-config-prettier@9.0.0(eslint@8.51.0): + /eslint-config-prettier@9.0.0(eslint@8.52.0): resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.51.0 + eslint: 8.52.0 dev: true /eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} dependencies: debug: 3.2.7 - is-core-module: 2.13.0 + is-core-module: 2.13.1 resolve: 1.22.8 transitivePeerDependencies: - supports-color dev: true - /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.8.0)(eslint-plugin-import@2.28.1)(eslint@8.51.0): + /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.9.1)(eslint-plugin-import@2.29.0)(eslint@8.52.0): resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -3175,12 +3203,12 @@ packages: dependencies: debug: 4.3.4 enhanced-resolve: 5.15.0 - eslint: 8.51.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.8.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.51.0) - eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.8.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.51.0) + eslint: 8.52.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) fast-glob: 3.3.1 get-tsconfig: 4.7.2 - is-core-module: 2.13.0 + is-core-module: 2.13.1 is-glob: 4.0.3 transitivePeerDependencies: - '@typescript-eslint/parser' @@ -3189,7 +3217,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.8.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.51.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -3210,17 +3238,17 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.8.0(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.9.1(eslint@8.52.0)(typescript@5.2.2) debug: 3.2.7 - eslint: 8.51.0 + eslint: 8.52.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.8.0)(eslint-plugin-import@2.28.1)(eslint@8.51.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.9.1)(eslint-plugin-import@2.29.0)(eslint@8.52.0) transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.8.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.51.0): - resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} + /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0): + resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -3229,18 +3257,18 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.8.0(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.9.1(eslint@8.52.0)(typescript@5.2.2) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.51.0 + eslint: 8.52.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.8.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.51.0) - has: 1.0.4 - is-core-module: 2.13.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + hasown: 2.0.0 + is-core-module: 2.13.1 is-glob: 4.0.3 minimatch: 3.1.2 object.fromentries: 2.0.7 @@ -3254,7 +3282,7 @@ packages: - supports-color dev: true - /eslint-plugin-prettier@4.2.1(eslint-config-prettier@9.0.0)(eslint@8.51.0)(prettier@2.8.8): + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@9.0.0)(eslint@8.52.0)(prettier@2.8.8): resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -3265,8 +3293,8 @@ packages: eslint-config-prettier: optional: true dependencies: - eslint: 8.51.0 - eslint-config-prettier: 9.0.0(eslint@8.51.0) + eslint: 8.52.0 + eslint-config-prettier: 9.0.0(eslint@8.52.0) prettier: 2.8.8 prettier-linter-helpers: 1.0.0 dev: true @@ -3292,18 +3320,19 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.51.0: - resolution: {integrity: sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==} + /eslint@8.52.0: + resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.51.0) - '@eslint-community/regexpp': 4.9.1 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@eslint-community/regexpp': 4.10.0 '@eslint/eslintrc': 2.1.2 - '@eslint/js': 8.51.0 - '@humanwhocodes/config-array': 0.11.11 + '@eslint/js': 8.52.0 + '@humanwhocodes/config-array': 0.11.13 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 @@ -3347,8 +3376,8 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.10.0 - acorn-jsx: 5.3.2(acorn@8.10.0) + acorn: 8.11.2 + acorn-jsx: 5.3.2(acorn@8.11.2) eslint-visitor-keys: 3.4.3 dev: true @@ -3538,8 +3567,8 @@ packages: bser: 2.1.1 dev: true - /figlet@1.6.0: - resolution: {integrity: sha512-31EQGhCEITv6+hi2ORRPyn3bulaV9Fl4xOdR169cBzH/n1UqcxsiSB/noo6SJdD7Kfb1Ljit+IgR1USvF/XbdA==} + /figlet@1.7.0: + resolution: {integrity: sha512-gO8l3wvqo0V7wEFLXPbkX83b7MVjRrk1oRLfYlZXol8nEpb/ON9pcKLI4qpBv5YtOTfrINtqb7b40iYY2FTWFg==} engines: {node: '>= 0.4.0'} hasBin: true dev: false @@ -3622,8 +3651,16 @@ packages: is-callable: 1.2.7 dev: true - /fork-ts-checker-webpack-plugin@8.0.0(typescript@5.2.2)(webpack@5.88.2): - resolution: {integrity: sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==} + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + + /fork-ts-checker-webpack-plugin@9.0.2(typescript@5.2.2)(webpack@5.89.0): + resolution: {integrity: sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==} engines: {node: '>=12.13.0', yarn: '>=1.0.0'} peerDependencies: typescript: '>3.6.0' @@ -3632,7 +3669,7 @@ packages: '@babel/code-frame': 7.22.13 chalk: 4.1.2 chokidar: 3.5.3 - cosmiconfig: 7.1.0 + cosmiconfig: 8.3.6(typescript@5.2.2) deepmerge: 4.3.1 fs-extra: 10.1.0 memfs: 3.5.3 @@ -3642,7 +3679,7 @@ packages: semver: 7.5.4 tapable: 2.2.1 typescript: 5.2.2 - webpack: 5.88.2 + webpack: 5.89.0 dev: true /form-data@4.0.0: @@ -3681,7 +3718,7 @@ packages: dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 - universalify: 2.0.0 + universalify: 2.0.1 dev: true /fs-extra@11.1.1: @@ -3690,7 +3727,7 @@ packages: dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 - universalify: 2.0.0 + universalify: 2.0.1 dev: false /fs-minipass@2.1.0: @@ -3721,9 +3758,9 @@ packages: resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 - es-abstract: 1.22.2 + es-abstract: 1.22.3 functions-have-names: 1.2.3 dev: true @@ -3755,13 +3792,13 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - /get-intrinsic@1.2.1: - resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} dependencies: function-bind: 1.1.2 - has: 1.0.4 has-proto: 1.0.1 has-symbols: 1.0.3 + hasown: 2.0.0 /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} @@ -3783,8 +3820,8 @@ packages: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 + call-bind: 1.0.5 + get-intrinsic: 1.2.2 dev: true /get-tsconfig@4.7.2: @@ -3818,6 +3855,18 @@ packages: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} dev: true + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: true + /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: @@ -3882,8 +3931,7 @@ packages: /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: - get-intrinsic: 1.2.1 - dev: true + get-intrinsic: 1.2.2 /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -3910,11 +3958,10 @@ packages: engines: {node: '>=8'} dev: true - /has-property-descriptors@1.0.0: - resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} dependencies: - get-intrinsic: 1.2.1 - dev: true + get-intrinsic: 1.2.2 /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} @@ -3935,9 +3982,11 @@ packages: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} dev: false - /has@1.0.4: - resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} - engines: {node: '>= 0.4.0'} + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 /hexoid@1.0.0: resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} @@ -4068,12 +4117,12 @@ packages: wrap-ansi: 6.2.0 dev: true - /internal-slot@1.0.5: - resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + /internal-slot@1.0.6: + resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.2.1 - has: 1.0.4 + get-intrinsic: 1.2.2 + hasown: 2.0.0 side-channel: 1.0.4 dev: true @@ -4094,8 +4143,8 @@ packages: /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 + call-bind: 1.0.5 + get-intrinsic: 1.2.2 is-typed-array: 1.1.12 dev: true @@ -4123,7 +4172,7 @@ packages: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 has-tostringtag: 1.0.0 dev: true @@ -4132,10 +4181,10 @@ packages: engines: {node: '>= 0.4'} dev: true - /is-core-module@2.13.0: - resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: - has: 1.0.4 + hasown: 2.0.0 /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} @@ -4193,14 +4242,14 @@ packages: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 has-tostringtag: 1.0.0 dev: true /is-shared-array-buffer@1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 dev: true /is-stream@2.0.1: @@ -4226,7 +4275,7 @@ packages: resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} engines: {node: '>= 0.4'} dependencies: - which-typed-array: 1.1.11 + which-typed-array: 1.1.13 dev: true /is-unicode-supported@0.1.0: @@ -4237,7 +4286,7 @@ packages: /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 dev: true /isarray@1.0.0: @@ -4314,6 +4363,15 @@ packages: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + /jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4485,7 +4543,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/graceful-fs': 4.1.7 + '@types/graceful-fs': 4.1.8 '@types/node': 20.5.7 anymatch: 3.1.3 fb-watchman: 2.0.2 @@ -4523,7 +4581,7 @@ packages: dependencies: '@babel/code-frame': 7.22.13 '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.1 + '@types/stack-utils': 2.0.2 chalk: 4.1.2 graceful-fs: 4.2.11 micromatch: 4.0.5 @@ -4811,7 +4869,7 @@ packages: /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: - universalify: 2.0.0 + universalify: 2.0.1 optionalDependencies: graceful-fs: 4.2.11 @@ -4927,8 +4985,8 @@ packages: type-check: 0.4.0 dev: true - /libphonenumber-js@1.10.48: - resolution: {integrity: sha512-Vvcgt4+o8+puIBJZLdMshPYx9nRN3/kTT7HPtOyfYrSQuN9PGBF1KUv0g07fjNzt4E4GuA7FnsLb+WeAMzyRQg==} + /libphonenumber-js@1.10.49: + resolution: {integrity: sha512-gvLtyC3tIuqfPzjvYLH9BmVdqzGDiSi4VjtWe2fAgSdBf0yt8yPmbNnRIHNbR5IdtVkm0ayGuzwQKTWmU0hdjQ==} /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -5027,13 +5085,6 @@ packages: engines: {node: '>=6'} dev: true - /magic-string@0.30.0: - resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - /magic-string@0.30.1: resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==} engines: {node: '>=12'} @@ -5098,8 +5149,8 @@ packages: braces: 3.0.2 picomatch: 2.3.1 - /mikro-orm@5.8.9: - resolution: {integrity: sha512-zklwaoLL5budjVhfylNZGFmv7ysCCZFXgVzzzLDr7A67MNKTL2LFhWgRHqi7z/dIoAW+38o8uRkaUK9xpL9n0Q==} + /mikro-orm@5.9.1: + resolution: {integrity: sha512-wXUtVElIbrJgvXAR7gD1xnVdCMP4F8rZ3OwxeAAY7gmLgIQTgQPQiwXH/kqcfZ5rBBOBmc0cTnuIwxceaOAwQA==} engines: {node: '>= 14.0.0'} dev: false @@ -5160,6 +5211,13 @@ packages: brace-expansion: 2.0.1 dev: true + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -5256,8 +5314,8 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: true - /nestjs-i18n@10.3.6(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(class-validator@0.14.0)(rxjs@7.8.1): - resolution: {integrity: sha512-/vYS1w2zaRR22I63fzJ8mFwIEfZOlKDEe2AAuNUNMAt60TkWShgm4xWWQzOgZ9jOYpHMpjFTAwA1WV7RaYbE8w==} + /nestjs-i18n@10.3.7(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(class-validator@0.14.0)(rxjs@7.8.1): + resolution: {integrity: sha512-zapq3b9sKonfD8C2OL6AdZTJzhjQQHeUJ4dWnI2TEnVe2g3k41+ibrNRDvUlbb3YsIMolvhgV4PKMDAZRCvtoQ==} engines: {node: '>=16'} peerDependencies: '@nestjs/common': '*' @@ -5265,8 +5323,8 @@ packages: class-validator: '*' rxjs: '*' dependencies: - '@nestjs/common': 10.2.7(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) accept-language-parser: 1.5.0 chokidar: 3.5.3 class-validator: 0.14.0 @@ -5321,8 +5379,8 @@ packages: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: true - /nodemailer@6.9.6: - resolution: {integrity: sha512-s7pDtWwe5fLMkQUhw8TkWB/wnZ7SRdd9HRZslq/s24hlZvBP3j32N/ETLmnqTpmj4xoBZL9fOWyCIZ7r2HORHg==} + /nodemailer@6.9.7: + resolution: {integrity: sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==} engines: {node: '>=6.0.0'} dev: false @@ -5358,8 +5416,8 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - /object-inspect@1.13.0: - resolution: {integrity: sha512-HQ4J+ic8hKrgIt3mqk6cVOVrW2ozL4KdvHlqpBv9vDYWx9ysAgENAdvy4FoGF+KFdhR7nQTNm5J0ctAeOwn+3g==} + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} @@ -5370,7 +5428,7 @@ packages: resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 has-symbols: 1.0.3 object-keys: 1.1.1 @@ -5380,27 +5438,27 @@ packages: resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 - es-abstract: 1.22.2 + es-abstract: 1.22.3 dev: true /object.groupby@1.0.1: resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 - es-abstract: 1.22.2 - get-intrinsic: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 dev: true /object.values@1.1.7: resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 - es-abstract: 1.22.2 + es-abstract: 1.22.3 dev: true /on-finished@2.4.1: @@ -5790,8 +5848,8 @@ packages: end-of-stream: 1.4.4 once: 1.4.0 - /punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} dev: true @@ -5914,7 +5972,7 @@ packages: resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 set-function-name: 2.0.1 dev: true @@ -5962,7 +6020,7 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true dependencies: - is-core-module: 2.13.0 + is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -6011,8 +6069,8 @@ packages: resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} engines: {node: '>=0.4'} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 + call-bind: 1.0.5 + get-intrinsic: 1.2.2 has-symbols: 1.0.3 isarray: 2.0.5 dev: true @@ -6026,8 +6084,8 @@ packages: /safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 + call-bind: 1.0.5 + get-intrinsic: 1.2.2 is-regex: 1.1.4 dev: true @@ -6038,7 +6096,7 @@ packages: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/json-schema': 7.0.13 + '@types/json-schema': 7.0.14 ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) dev: true @@ -6095,13 +6153,22 @@ packages: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: false + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + /set-function-name@2.0.1: resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} engines: {node: '>= 0.4'} dependencies: define-data-property: 1.1.1 functions-have-names: 1.2.3 - has-property-descriptors: 1.0.0 + has-property-descriptors: 1.0.1 dev: true /setprototypeof@1.2.0: @@ -6147,13 +6214,18 @@ packages: /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - object-inspect: 1.13.0 + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + object-inspect: 1.13.1 /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + /simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} dev: false @@ -6232,8 +6304,8 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - /streamx@2.15.1: - resolution: {integrity: sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==} + /streamx@2.15.2: + resolution: {integrity: sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg==} dependencies: fast-fifo: 1.3.2 queue-tick: 1.0.1 @@ -6264,29 +6336,38 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + /string.prototype.trim@1.2.8: resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 - es-abstract: 1.22.2 + es-abstract: 1.22.3 dev: true /string.prototype.trimend@1.0.7: resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 - es-abstract: 1.22.2 + es-abstract: 1.22.3 dev: true /string.prototype.trimstart@1.0.7: resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 define-properties: 1.2.1 - es-abstract: 1.22.2 + es-abstract: 1.22.3 dev: true /string_decoder@1.1.1: @@ -6305,6 +6386,13 @@ packages: dependencies: ansi-regex: 5.0.1 + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -6436,7 +6524,7 @@ packages: dependencies: b4a: 1.6.4 fast-fifo: 1.3.2 - streamx: 2.15.1 + streamx: 2.15.2 dev: false /tar@6.2.0: @@ -6456,30 +6544,6 @@ packages: engines: {node: '>=8.0.0'} dev: false - /terser-webpack-plugin@5.3.9(webpack@5.88.2): - resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - dependencies: - '@jridgewell/trace-mapping': 0.3.19 - jest-worker: 27.5.1 - schema-utils: 3.3.0 - serialize-javascript: 6.0.1 - terser: 5.22.0 - webpack: 5.88.2 - dev: true - /terser-webpack-plugin@5.3.9(webpack@5.89.0): resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} engines: {node: '>= 10.13.0'} @@ -6496,21 +6560,21 @@ packages: uglify-js: optional: true dependencies: - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.20 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.1 - terser: 5.22.0 + terser: 5.24.0 webpack: 5.89.0 dev: true - /terser@5.22.0: - resolution: {integrity: sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==} + /terser@5.24.0: + resolution: {integrity: sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==} engines: {node: '>=10'} hasBin: true dependencies: '@jridgewell/source-map': 0.3.5 - acorn: 8.10.0 + acorn: 8.11.2 commander: 2.20.3 source-map-support: 0.5.21 dev: true @@ -6665,8 +6729,8 @@ packages: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 20.5.7 - acorn: 8.10.0 - acorn-walk: 8.2.0 + acorn: 8.11.2 + acorn-walk: 8.3.0 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 @@ -6743,6 +6807,11 @@ packages: engines: {node: '>=16'} dev: false + /type-fest@4.6.0: + resolution: {integrity: sha512-rLjWJzQFOq4xw7MgJrCZ6T1jIOvvYElXT12r+y0CC6u67hegDHaxcPqb2fZHOGlqxugGQPNB1EnTezjBetkwkw==} + engines: {node: '>=16'} + dev: false + /type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -6754,8 +6823,8 @@ packages: resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 + call-bind: 1.0.5 + get-intrinsic: 1.2.2 is-typed-array: 1.1.12 dev: true @@ -6763,7 +6832,7 @@ packages: resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 for-each: 0.3.3 has-proto: 1.0.1 is-typed-array: 1.1.12 @@ -6774,7 +6843,7 @@ packages: engines: {node: '>= 0.4'} dependencies: available-typed-arrays: 1.0.5 - call-bind: 1.0.2 + call-bind: 1.0.5 for-each: 0.3.3 has-proto: 1.0.1 is-typed-array: 1.1.12 @@ -6783,7 +6852,7 @@ packages: /typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 for-each: 0.3.3 is-typed-array: 1.1.12 dev: true @@ -6807,7 +6876,7 @@ packages: resolution: {integrity: sha512-jG3C35jti1YnCuH/k3fJEfHbnIG9c3Q9ITZ0B9eWwnXngh/AUd0mRHv8OdpE2Q9VoK7tB6xL990JrMCr0LtfNA==} engines: {node: '>=12'} dependencies: - '@rushstack/ts-command-line': 4.16.1 + '@rushstack/ts-command-line': 4.17.1 emittery: 0.13.1 glob: 8.1.0 pony-cause: 2.1.10 @@ -6817,14 +6886,14 @@ packages: /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.5 has-bigints: 1.0.2 has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 dev: true - /universalify@2.0.0: - resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} /unpipe@1.0.0: @@ -6845,7 +6914,7 @@ packages: /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: - punycode: 2.3.0 + punycode: 2.3.1 dev: true /util-deprecate@1.0.2: @@ -6873,8 +6942,8 @@ packages: resolution: {integrity: sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==} engines: {node: '>=10.12.0'} dependencies: - '@jridgewell/trace-mapping': 0.3.19 - '@types/istanbul-lib-coverage': 2.0.4 + '@jridgewell/trace-mapping': 0.3.20 + '@types/istanbul-lib-coverage': 2.0.5 convert-source-map: 2.0.0 dev: true @@ -6919,46 +6988,6 @@ packages: engines: {node: '>=10.13.0'} dev: true - /webpack@5.88.2: - resolution: {integrity: sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - dependencies: - '@types/eslint-scope': 3.7.5 - '@types/estree': 1.0.2 - '@webassemblyjs/ast': 1.11.6 - '@webassemblyjs/wasm-edit': 1.11.6 - '@webassemblyjs/wasm-parser': 1.11.6 - acorn: 8.10.0 - acorn-import-assertions: 1.9.0(acorn@8.10.0) - browserslist: 4.22.1 - chrome-trace-event: 1.0.3 - enhanced-resolve: 5.15.0 - es-module-lexer: 1.3.1 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 3.3.0 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.9(webpack@5.88.2) - watchpack: 2.4.0 - webpack-sources: 3.2.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - dev: true - /webpack@5.89.0: resolution: {integrity: sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==} engines: {node: '>=10.13.0'} @@ -6969,13 +6998,13 @@ packages: webpack-cli: optional: true dependencies: - '@types/eslint-scope': 3.7.5 - '@types/estree': 1.0.2 + '@types/eslint-scope': 3.7.6 + '@types/estree': 1.0.4 '@webassemblyjs/ast': 1.11.6 '@webassemblyjs/wasm-edit': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6 - acorn: 8.10.0 - acorn-import-assertions: 1.9.0(acorn@8.10.0) + acorn: 8.11.2 + acorn-import-assertions: 1.9.0(acorn@8.11.2) browserslist: 4.22.1 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 @@ -7015,12 +7044,12 @@ packages: is-symbol: 1.0.4 dev: true - /which-typed-array@1.1.11: - resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + /which-typed-array@1.1.13: + resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} engines: {node: '>= 0.4'} dependencies: available-typed-arrays: 1.0.5 - call-bind: 1.0.2 + call-bind: 1.0.5 for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 @@ -7064,6 +7093,15 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -7090,11 +7128,6 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - /yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - dev: true - /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} From cc47277af2a86709a6772266e7c24ed2fb64139e Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Tue, 7 Nov 2023 18:44:44 +0100 Subject: [PATCH 08/15] refactor(dirs): renamed `_mixin` to `base` --- src/main.ts | 2 +- src/modules/auth/auth.controller.ts | 4 ++-- src/modules/auth/auth.service.ts | 2 +- src/modules/auth/dto/input.dto.ts | 2 +- src/modules/auth/guards/self.guard.ts | 2 +- .../{_mixin => base}/decorators/api-not-ok.decorator.ts | 0 src/modules/{_mixin => base}/decorators/index.ts | 0 .../{_mixin => base}/decorators/is-boolean.decorator.ts | 0 .../{_mixin => base}/decorators/is-date.decorator.ts | 0 .../{_mixin => base}/decorators/is-email.decorator.ts | 0 .../{_mixin => base}/decorators/is-id.decorator.ts | 0 .../{_mixin => base}/decorators/is-phone.decorator.ts | 0 .../{_mixin => base}/decorators/is-string.decorator.ts | 0 .../decorators/is-strong-pass.decorator.ts | 0 src/modules/{_mixin => base}/dto/input.dto.ts | 2 +- src/modules/{_mixin => base}/dto/output.dto.ts | 0 src/modules/{_mixin => base}/entities/base.entity.ts | 0 src/modules/{_mixin => base}/http-errors/bad-request.ts | 0 src/modules/{_mixin => base}/http-errors/base.ts | 0 src/modules/{_mixin => base}/http-errors/forbidden.ts | 0 src/modules/{_mixin => base}/http-errors/index.ts | 0 src/modules/{_mixin => base}/http-errors/not-found.ts | 0 src/modules/{_mixin => base}/http-errors/unauthorized.ts | 0 src/modules/emails/emails.service.ts | 2 +- src/modules/files/entities/file-visibility.entity.ts | 2 +- src/modules/files/entities/file.entity.ts | 2 +- src/modules/files/files.service.ts | 2 +- src/modules/files/images.service.ts | 2 +- src/modules/logs/entities/log.entity.ts | 2 +- src/modules/logs/logs.controller.ts | 6 +++--- src/modules/logs/logs.service.ts | 2 +- src/modules/permissions/dto/input.dto.ts | 2 +- src/modules/permissions/dto/output.dto.ts | 2 +- src/modules/permissions/entities/permission.entity.ts | 2 +- src/modules/permissions/permissions.controller.ts | 4 ++-- src/modules/permissions/permissions.service.ts | 2 +- src/modules/promotions/dto/input.dto.ts | 2 +- src/modules/promotions/dto/output.dto.ts | 2 +- src/modules/promotions/entities/promotion.entity.ts | 2 +- src/modules/promotions/promotions.controller.ts | 6 +++--- src/modules/promotions/promotions.service.ts | 4 ++-- src/modules/roles/dto/input.dto.ts | 2 +- src/modules/roles/entities/role-expiration.entity.ts | 2 +- src/modules/roles/entities/role.entity.ts | 2 +- src/modules/roles/roles.controller.ts | 4 ++-- src/modules/roles/roles.service.ts | 2 +- src/modules/users/controllers/users-data.controller.ts | 6 +++--- src/modules/users/controllers/users-files.controller.ts | 8 ++++---- src/modules/users/dto/input.dto.ts | 2 +- src/modules/users/dto/output.dto.ts | 2 +- src/modules/users/entities/user-visibility.entity.ts | 2 +- src/modules/users/entities/user.entity.ts | 2 +- src/modules/users/services/users-data.service.ts | 6 +++--- src/modules/users/services/users-files.service.ts | 4 ++-- tests/e2e/auth.e2e-spec.ts | 8 ++++---- tests/e2e/logs.e2e-spec.ts | 4 ++-- tests/e2e/permissions.e2e-spec.ts | 2 +- tests/e2e/promotions.e2e-spec.ts | 4 ++-- tests/e2e/roles.e2e-spec.ts | 2 +- tests/e2e/users/users-data.e2e-spec.ts | 4 ++-- tests/e2e/users/users-files.e2e-spec.ts | 4 ++-- tests/index.ts | 2 +- tests/units/services/auth.test.ts | 2 +- tests/units/services/files.test.ts | 2 +- tests/units/services/images.test.ts | 2 +- tests/units/services/users/users-data.test.ts | 2 +- tests/units/utils/password.test.ts | 2 +- 67 files changed, 73 insertions(+), 73 deletions(-) rename src/modules/{_mixin => base}/decorators/api-not-ok.decorator.ts (100%) rename src/modules/{_mixin => base}/decorators/index.ts (100%) rename src/modules/{_mixin => base}/decorators/is-boolean.decorator.ts (100%) rename src/modules/{_mixin => base}/decorators/is-date.decorator.ts (100%) rename src/modules/{_mixin => base}/decorators/is-email.decorator.ts (100%) rename src/modules/{_mixin => base}/decorators/is-id.decorator.ts (100%) rename src/modules/{_mixin => base}/decorators/is-phone.decorator.ts (100%) rename src/modules/{_mixin => base}/decorators/is-string.decorator.ts (100%) rename src/modules/{_mixin => base}/decorators/is-strong-pass.decorator.ts (100%) rename src/modules/{_mixin => base}/dto/input.dto.ts (71%) rename src/modules/{_mixin => base}/dto/output.dto.ts (100%) rename src/modules/{_mixin => base}/entities/base.entity.ts (100%) rename src/modules/{_mixin => base}/http-errors/bad-request.ts (100%) rename src/modules/{_mixin => base}/http-errors/base.ts (100%) rename src/modules/{_mixin => base}/http-errors/forbidden.ts (100%) rename src/modules/{_mixin => base}/http-errors/index.ts (100%) rename src/modules/{_mixin => base}/http-errors/not-found.ts (100%) rename src/modules/{_mixin => base}/http-errors/unauthorized.ts (100%) diff --git a/src/main.ts b/src/main.ts index 56b46f37..703d236f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,7 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n'; import { VALIDATION_PIPE_OPTIONS, env } from '@env'; -import { I18nHttpExceptionFilter } from '@modules/_mixin/http-errors'; +import { I18nHttpExceptionFilter } from '@modules/base/http-errors'; import '@exported/global/utils'; import { AppModule } from './app.module'; diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 1276b3e7..2b8d5467 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,8 +1,8 @@ import { Controller, Post, Body, Param, Get } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; -import { OutputCreatedDTO, OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; +import { OutputCreatedDTO, OutputMessageDTO } from '@modules/base/dto/output.dto'; import { UsersDataService } from '@modules/users/services/users-data.service'; import { AuthService } from './auth.service'; diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index aa823d36..004eda60 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -7,7 +7,7 @@ import { JwtService } from '@nestjs/jwt'; import { compareSync } from 'bcrypt'; import { env } from '@env'; -import { i18nForbiddenException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; +import { i18nForbiddenException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/base/http-errors'; import { User } from '@modules/users/entities/user.entity'; import { OutputTokenDTO } from './dto/output.dto'; diff --git a/src/modules/auth/dto/input.dto.ts b/src/modules/auth/dto/input.dto.ts index 1571212c..81eb8106 100644 --- a/src/modules/auth/dto/input.dto.ts +++ b/src/modules/auth/dto/input.dto.ts @@ -12,7 +12,7 @@ import { Type } from 'class-transformer'; import { ArrayNotEmpty, ArrayUnique, ValidateNested } from 'class-validator'; import { i18nValidationMessage } from 'nestjs-i18n'; -import { I18nIsId, I18nIsEmail, I18nIsDate, I18nIsString, I18nIsStrongPassword } from '@modules/_mixin/decorators'; +import { I18nIsId, I18nIsEmail, I18nIsDate, I18nIsString, I18nIsStrongPassword } from '@modules/base/decorators'; export class InputRegisterUserAdminDTO implements InputRegisterUserAdminDto { @ApiProperty({ example: 'example@domain.com' }) diff --git a/src/modules/auth/guards/self.guard.ts b/src/modules/auth/guards/self.guard.ts index f7ff8905..c543ce49 100644 --- a/src/modules/auth/guards/self.guard.ts +++ b/src/modules/auth/guards/self.guard.ts @@ -4,7 +4,7 @@ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { z } from 'zod'; -import { i18nBadRequestException } from '@modules/_mixin/http-errors'; +import { i18nBadRequestException } from '@modules/base/http-errors'; import { AuthService } from '../auth.service'; diff --git a/src/modules/_mixin/decorators/api-not-ok.decorator.ts b/src/modules/base/decorators/api-not-ok.decorator.ts similarity index 100% rename from src/modules/_mixin/decorators/api-not-ok.decorator.ts rename to src/modules/base/decorators/api-not-ok.decorator.ts diff --git a/src/modules/_mixin/decorators/index.ts b/src/modules/base/decorators/index.ts similarity index 100% rename from src/modules/_mixin/decorators/index.ts rename to src/modules/base/decorators/index.ts diff --git a/src/modules/_mixin/decorators/is-boolean.decorator.ts b/src/modules/base/decorators/is-boolean.decorator.ts similarity index 100% rename from src/modules/_mixin/decorators/is-boolean.decorator.ts rename to src/modules/base/decorators/is-boolean.decorator.ts diff --git a/src/modules/_mixin/decorators/is-date.decorator.ts b/src/modules/base/decorators/is-date.decorator.ts similarity index 100% rename from src/modules/_mixin/decorators/is-date.decorator.ts rename to src/modules/base/decorators/is-date.decorator.ts diff --git a/src/modules/_mixin/decorators/is-email.decorator.ts b/src/modules/base/decorators/is-email.decorator.ts similarity index 100% rename from src/modules/_mixin/decorators/is-email.decorator.ts rename to src/modules/base/decorators/is-email.decorator.ts diff --git a/src/modules/_mixin/decorators/is-id.decorator.ts b/src/modules/base/decorators/is-id.decorator.ts similarity index 100% rename from src/modules/_mixin/decorators/is-id.decorator.ts rename to src/modules/base/decorators/is-id.decorator.ts diff --git a/src/modules/_mixin/decorators/is-phone.decorator.ts b/src/modules/base/decorators/is-phone.decorator.ts similarity index 100% rename from src/modules/_mixin/decorators/is-phone.decorator.ts rename to src/modules/base/decorators/is-phone.decorator.ts diff --git a/src/modules/_mixin/decorators/is-string.decorator.ts b/src/modules/base/decorators/is-string.decorator.ts similarity index 100% rename from src/modules/_mixin/decorators/is-string.decorator.ts rename to src/modules/base/decorators/is-string.decorator.ts diff --git a/src/modules/_mixin/decorators/is-strong-pass.decorator.ts b/src/modules/base/decorators/is-strong-pass.decorator.ts similarity index 100% rename from src/modules/_mixin/decorators/is-strong-pass.decorator.ts rename to src/modules/base/decorators/is-strong-pass.decorator.ts diff --git a/src/modules/_mixin/dto/input.dto.ts b/src/modules/base/dto/input.dto.ts similarity index 71% rename from src/modules/_mixin/dto/input.dto.ts rename to src/modules/base/dto/input.dto.ts index b6d1d107..b5b59e1f 100644 --- a/src/modules/_mixin/dto/input.dto.ts +++ b/src/modules/base/dto/input.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { I18nIsId } from '@modules/_mixin/decorators'; +import { I18nIsId } from '@modules/base/decorators'; export class InputIdParamDTO { @ApiProperty({ minimum: 1 }) diff --git a/src/modules/_mixin/dto/output.dto.ts b/src/modules/base/dto/output.dto.ts similarity index 100% rename from src/modules/_mixin/dto/output.dto.ts rename to src/modules/base/dto/output.dto.ts diff --git a/src/modules/_mixin/entities/base.entity.ts b/src/modules/base/entities/base.entity.ts similarity index 100% rename from src/modules/_mixin/entities/base.entity.ts rename to src/modules/base/entities/base.entity.ts diff --git a/src/modules/_mixin/http-errors/bad-request.ts b/src/modules/base/http-errors/bad-request.ts similarity index 100% rename from src/modules/_mixin/http-errors/bad-request.ts rename to src/modules/base/http-errors/bad-request.ts diff --git a/src/modules/_mixin/http-errors/base.ts b/src/modules/base/http-errors/base.ts similarity index 100% rename from src/modules/_mixin/http-errors/base.ts rename to src/modules/base/http-errors/base.ts diff --git a/src/modules/_mixin/http-errors/forbidden.ts b/src/modules/base/http-errors/forbidden.ts similarity index 100% rename from src/modules/_mixin/http-errors/forbidden.ts rename to src/modules/base/http-errors/forbidden.ts diff --git a/src/modules/_mixin/http-errors/index.ts b/src/modules/base/http-errors/index.ts similarity index 100% rename from src/modules/_mixin/http-errors/index.ts rename to src/modules/base/http-errors/index.ts diff --git a/src/modules/_mixin/http-errors/not-found.ts b/src/modules/base/http-errors/not-found.ts similarity index 100% rename from src/modules/_mixin/http-errors/not-found.ts rename to src/modules/base/http-errors/not-found.ts diff --git a/src/modules/_mixin/http-errors/unauthorized.ts b/src/modules/base/http-errors/unauthorized.ts similarity index 100% rename from src/modules/_mixin/http-errors/unauthorized.ts rename to src/modules/base/http-errors/unauthorized.ts diff --git a/src/modules/emails/emails.service.ts b/src/modules/emails/emails.service.ts index 4cac507f..fc3227f5 100644 --- a/src/modules/emails/emails.service.ts +++ b/src/modules/emails/emails.service.ts @@ -7,7 +7,7 @@ import { Injectable } from '@nestjs/common'; import { Transporter, createTransport } from 'nodemailer'; import { env } from '@env'; -import { i18nBadRequestException } from '@modules/_mixin/http-errors'; +import { i18nBadRequestException } from '@modules/base/http-errors'; interface EmailOptions { to: string[]; diff --git a/src/modules/files/entities/file-visibility.entity.ts b/src/modules/files/entities/file-visibility.entity.ts index c2739fd8..1f0ebe63 100644 --- a/src/modules/files/entities/file-visibility.entity.ts +++ b/src/modules/files/entities/file-visibility.entity.ts @@ -1,6 +1,6 @@ import { Collection, Entity, ManyToMany, OneToMany, Property } from '@mikro-orm/core'; -import { BaseEntity } from '@modules/_mixin/entities/base.entity'; +import { BaseEntity } from '@modules/base/entities/base.entity'; import { File } from '@modules/files/entities/file.entity'; import { User } from '@modules/users/entities/user.entity'; diff --git a/src/modules/files/entities/file.entity.ts b/src/modules/files/entities/file.entity.ts index 754b1047..de5517c4 100644 --- a/src/modules/files/entities/file.entity.ts +++ b/src/modules/files/entities/file.entity.ts @@ -1,6 +1,6 @@ import { Entity, ManyToOne, Property } from '@mikro-orm/core'; -import { BaseEntity } from '@modules/_mixin/entities/base.entity'; +import { BaseEntity } from '@modules/base/entities/base.entity'; import { FileVisibilityGroup } from '@modules/files/entities/file-visibility.entity'; import { PromotionPicture } from '@modules/promotions/entities/promotion-picture.entity'; import { UserBanner } from '@modules/users/entities/user-banner.entity'; diff --git a/src/modules/files/files.service.ts b/src/modules/files/files.service.ts index c84559ba..62044d6c 100644 --- a/src/modules/files/files.service.ts +++ b/src/modules/files/files.service.ts @@ -7,7 +7,7 @@ import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; import { Injectable, StreamableFile } from '@nestjs/common'; import { fromBuffer, MimeType } from 'file-type'; -import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; +import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/base/http-errors'; import { User } from '@modules/users/entities/user.entity'; import { UsersDataService } from '@modules/users/services/users-data.service'; diff --git a/src/modules/files/images.service.ts b/src/modules/files/images.service.ts index 7ed79022..c7974f46 100644 --- a/src/modules/files/images.service.ts +++ b/src/modules/files/images.service.ts @@ -4,7 +4,7 @@ import { Injectable } from '@nestjs/common'; import { fromBuffer } from 'file-type'; import sharp from 'sharp'; -import { i18nBadRequestException } from '@modules/_mixin/http-errors'; +import { i18nBadRequestException } from '@modules/base/http-errors'; import { FilesService, WriteFileOptions } from './files.service'; diff --git a/src/modules/logs/entities/log.entity.ts b/src/modules/logs/entities/log.entity.ts index 6b8dc042..761c8b9c 100644 --- a/src/modules/logs/entities/log.entity.ts +++ b/src/modules/logs/entities/log.entity.ts @@ -1,6 +1,6 @@ import { Entity, ManyToOne, Property } from '@mikro-orm/core'; -import { BaseEntity } from '@modules/_mixin/entities/base.entity'; +import { BaseEntity } from '@modules/base/entities/base.entity'; import { User } from '@modules/users/entities/user.entity'; @Entity({ tableName: 'users_logs' }) diff --git a/src/modules/logs/logs.controller.ts b/src/modules/logs/logs.controller.ts index dbc32da4..a6d47b80 100644 --- a/src/modules/logs/logs.controller.ts +++ b/src/modules/logs/logs.controller.ts @@ -2,9 +2,9 @@ import { Controller, Delete, Get, Param, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiTags, ApiBearerAuth, ApiOkResponse, ApiParam, ApiOperation } from '@nestjs/swagger'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; -import { InputIdParamDTO } from '@modules/_mixin/dto/input.dto'; -import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/base/dto/input.dto'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; diff --git a/src/modules/logs/logs.service.ts b/src/modules/logs/logs.service.ts index caa73a72..d24b5b6e 100644 --- a/src/modules/logs/logs.service.ts +++ b/src/modules/logs/logs.service.ts @@ -2,7 +2,7 @@ import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; import { OutputLogDTO } from './dto/output.dto'; import { Log } from './entities/log.entity'; diff --git a/src/modules/permissions/dto/input.dto.ts b/src/modules/permissions/dto/input.dto.ts index 2ca8a9c8..3a595dfd 100644 --- a/src/modules/permissions/dto/input.dto.ts +++ b/src/modules/permissions/dto/input.dto.ts @@ -10,7 +10,7 @@ import { IsIn } from 'class-validator'; import { i18nValidationMessage } from 'nestjs-i18n'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -import { I18nIsDate, I18nIsId, I18nIsBoolean } from '@modules/_mixin/decorators'; +import { I18nIsDate, I18nIsId, I18nIsBoolean } from '@modules/base/decorators'; export class InputCreatePermissionDTO implements InputCreatePermissionDto { @ApiProperty() diff --git a/src/modules/permissions/dto/output.dto.ts b/src/modules/permissions/dto/output.dto.ts index d3ac3ef1..83ab8b04 100644 --- a/src/modules/permissions/dto/output.dto.ts +++ b/src/modules/permissions/dto/output.dto.ts @@ -3,7 +3,7 @@ import type { OutputPermissionDto, OutputPermissionsOfRoleDto, PERMISSION_NAMES import { ApiProperty } from '@nestjs/swagger'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -import { OutputBaseDTO } from '@modules/_mixin/dto/output.dto'; +import { OutputBaseDTO } from '@modules/base/dto/output.dto'; export class OutputPermissionsOfRoleDTO implements OutputPermissionsOfRoleDto { @ApiProperty() diff --git a/src/modules/permissions/entities/permission.entity.ts b/src/modules/permissions/entities/permission.entity.ts index cc5fd373..60f7253f 100644 --- a/src/modules/permissions/entities/permission.entity.ts +++ b/src/modules/permissions/entities/permission.entity.ts @@ -2,7 +2,7 @@ import type { PERMISSION_NAMES } from '#types/api'; import { Entity, ManyToOne, Property } from '@mikro-orm/core'; -import { BaseEntity } from '@modules/_mixin/entities/base.entity'; +import { BaseEntity } from '@modules/base/entities/base.entity'; import { User } from '@modules/users/entities/user.entity'; @Entity({ tableName: 'permissions' }) diff --git a/src/modules/permissions/permissions.controller.ts b/src/modules/permissions/permissions.controller.ts index cb5026e9..44bf4a2b 100644 --- a/src/modules/permissions/permissions.controller.ts +++ b/src/modules/permissions/permissions.controller.ts @@ -2,8 +2,8 @@ import { Body, Controller, Get, Param, Post, UseGuards, Patch } from '@nestjs/co import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; -import { InputIdParamDTO } from '@modules/_mixin/dto/input.dto'; +import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/base/dto/input.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; diff --git a/src/modules/permissions/permissions.service.ts b/src/modules/permissions/permissions.service.ts index 1c89a955..61d1468e 100644 --- a/src/modules/permissions/permissions.service.ts +++ b/src/modules/permissions/permissions.service.ts @@ -2,7 +2,7 @@ import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { i18nBadRequestException, i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/base/http-errors'; import { InputUpdatePermissionDTO, InputCreatePermissionDTO } from './dto/input.dto'; import { OutputPermissionDTO } from './dto/output.dto'; diff --git a/src/modules/promotions/dto/input.dto.ts b/src/modules/promotions/dto/input.dto.ts index 2a88b1f5..c807c50a 100644 --- a/src/modules/promotions/dto/input.dto.ts +++ b/src/modules/promotions/dto/input.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { I18nIsId } from '@modules/_mixin/decorators'; +import { I18nIsId } from '@modules/base/decorators'; export class InputPromotionNumberParamDTO { @ApiProperty({ minimum: 1 }) diff --git a/src/modules/promotions/dto/output.dto.ts b/src/modules/promotions/dto/output.dto.ts index 449b508b..2147d5b5 100644 --- a/src/modules/promotions/dto/output.dto.ts +++ b/src/modules/promotions/dto/output.dto.ts @@ -2,7 +2,7 @@ import type { OutputPromotionPictureDto, OutputPromotionDto } from '#types/api'; import { ApiProperty } from '@nestjs/swagger'; -import { OutputBaseDTO } from '@modules/_mixin/dto/output.dto'; +import { OutputBaseDTO } from '@modules/base/dto/output.dto'; import { OutputFileDTO } from '@modules/files/dto/output.dto'; export class OutputPromotionDTO extends OutputBaseDTO implements OutputPromotionDto { diff --git a/src/modules/promotions/entities/promotion.entity.ts b/src/modules/promotions/entities/promotion.entity.ts index 0e15f332..f142ba49 100644 --- a/src/modules/promotions/entities/promotion.entity.ts +++ b/src/modules/promotions/entities/promotion.entity.ts @@ -1,6 +1,6 @@ import { Cascade, Collection, Entity, OneToMany, OneToOne, Property } from '@mikro-orm/core'; -import { BaseEntity } from '@modules/_mixin/entities/base.entity'; +import { BaseEntity } from '@modules/base/entities/base.entity'; import { User } from '@modules/users/entities/user.entity'; import { PromotionPicture } from './promotion-picture.entity'; diff --git a/src/modules/promotions/promotions.controller.ts b/src/modules/promotions/promotions.controller.ts index d2af4994..09f4e4a4 100644 --- a/src/modules/promotions/promotions.controller.ts +++ b/src/modules/promotions/promotions.controller.ts @@ -2,9 +2,9 @@ import { Controller, Delete, Get, Param, Post, Req, UploadedFile, UseGuards } fr import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; -import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; -import { i18nBadRequestException } from '@modules/_mixin/http-errors'; +import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; +import { i18nBadRequestException } from '@modules/base/http-errors'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { ApiDownloadFile } from '@modules/files/decorators/download.decorator'; diff --git a/src/modules/promotions/promotions.service.ts b/src/modules/promotions/promotions.service.ts index bc2172d0..d5f696b9 100644 --- a/src/modules/promotions/promotions.service.ts +++ b/src/modules/promotions/promotions.service.ts @@ -5,8 +5,8 @@ import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { env } from '@env'; -import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; -import { i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; +import { i18nNotFoundException } from '@modules/base/http-errors'; import { ImagesService } from '@modules/files/images.service'; import { OutputPromotionPictureDTO, OutputPromotionDTO } from './dto/output.dto'; diff --git a/src/modules/roles/dto/input.dto.ts b/src/modules/roles/dto/input.dto.ts index ae16db27..b44ee5c9 100644 --- a/src/modules/roles/dto/input.dto.ts +++ b/src/modules/roles/dto/input.dto.ts @@ -14,7 +14,7 @@ import { ArrayNotEmpty, ArrayUnique, IsEnum, IsUppercase, ValidateNested } from import { i18nValidationMessage } from 'nestjs-i18n'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -import { I18nIsDate, I18nIsId, I18nIsString } from '@modules/_mixin/decorators'; +import { I18nIsDate, I18nIsId, I18nIsString } from '@modules/base/decorators'; export class InputCreateRoleDTO implements InputCreateRoleDto { @ApiProperty({ type: String, example: 'AE_ADMINS' }) diff --git a/src/modules/roles/entities/role-expiration.entity.ts b/src/modules/roles/entities/role-expiration.entity.ts index f9b0261d..1861654b 100644 --- a/src/modules/roles/entities/role-expiration.entity.ts +++ b/src/modules/roles/entities/role-expiration.entity.ts @@ -1,7 +1,7 @@ import { Entity, ManyToOne, Property } from '@mikro-orm/core'; import { ApiProperty } from '@nestjs/swagger'; -import { BaseEntity } from '@modules/_mixin/entities/base.entity'; +import { BaseEntity } from '@modules/base/entities/base.entity'; import { User } from '@modules/users/entities/user.entity'; import { Role } from './role.entity'; diff --git a/src/modules/roles/entities/role.entity.ts b/src/modules/roles/entities/role.entity.ts index 6ed4a4dd..2e55cc20 100644 --- a/src/modules/roles/entities/role.entity.ts +++ b/src/modules/roles/entities/role.entity.ts @@ -2,7 +2,7 @@ import type { PERMISSION_NAMES } from '#types/api'; import { Collection, Entity, ManyToMany, Property } from '@mikro-orm/core'; -import { BaseEntity } from '@modules/_mixin/entities/base.entity'; +import { BaseEntity } from '@modules/base/entities/base.entity'; import { User } from '@modules/users/entities/user.entity'; /** diff --git a/src/modules/roles/roles.controller.ts b/src/modules/roles/roles.controller.ts index e852b85e..f724ae93 100644 --- a/src/modules/roles/roles.controller.ts +++ b/src/modules/roles/roles.controller.ts @@ -2,8 +2,8 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@n import { AuthGuard } from '@nestjs/passport'; import { ApiTags, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiBody, ApiParam } from '@nestjs/swagger'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; -import { InputIdParamDTO } from '@modules/_mixin/dto/input.dto'; +import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/base/dto/input.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; diff --git a/src/modules/roles/roles.service.ts b/src/modules/roles/roles.service.ts index 7eea54e5..548d89b4 100644 --- a/src/modules/roles/roles.service.ts +++ b/src/modules/roles/roles.service.ts @@ -4,7 +4,7 @@ import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { i18nBadRequestException, i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/base/http-errors'; import { OutputBaseUserDTO } from '@modules/users/dto/output.dto'; import { User } from '@modules/users/entities/user.entity'; import { UsersDataService } from '@modules/users/services/users-data.service'; diff --git a/src/modules/users/controllers/users-data.controller.ts b/src/modules/users/controllers/users-data.controller.ts index d3b833db..e1356d66 100644 --- a/src/modules/users/controllers/users-data.controller.ts +++ b/src/modules/users/controllers/users-data.controller.ts @@ -2,9 +2,9 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Req, UseGuards } fro import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiBody, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; -import { InputIdParamDTO } from '@modules/_mixin/dto/input.dto'; -import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; +import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/base/dto/input.dto'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { GuardSelfParam } from '@modules/auth/decorators/self.decorator'; diff --git a/src/modules/users/controllers/users-files.controller.ts b/src/modules/users/controllers/users-files.controller.ts index c0c968a3..cc295aac 100644 --- a/src/modules/users/controllers/users-files.controller.ts +++ b/src/modules/users/controllers/users-files.controller.ts @@ -2,10 +2,10 @@ import { Controller, Delete, Get, Param, Post, Req, UploadedFile, UseGuards } fr import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { ApiNotOkResponses } from '@modules/_mixin/decorators/api-not-ok.decorator'; -import { InputIdParamDTO } from '@modules/_mixin/dto/input.dto'; -import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; -import { i18nBadRequestException } from '@modules/_mixin/http-errors'; +import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/base/dto/input.dto'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; +import { i18nBadRequestException } from '@modules/base/http-errors'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; diff --git a/src/modules/users/dto/input.dto.ts b/src/modules/users/dto/input.dto.ts index 63167f04..289e1782 100644 --- a/src/modules/users/dto/input.dto.ts +++ b/src/modules/users/dto/input.dto.ts @@ -13,7 +13,7 @@ import { I18nIsId, I18nIsPhoneNumber, I18nIsString, -} from '@modules/_mixin/decorators'; +} from '@modules/base/decorators'; export class InputUpdateUserDTO implements InputUpdateUserDto { @ApiProperty({ required: false }) diff --git a/src/modules/users/dto/output.dto.ts b/src/modules/users/dto/output.dto.ts index cd4d6ccc..eea6403d 100644 --- a/src/modules/users/dto/output.dto.ts +++ b/src/modules/users/dto/output.dto.ts @@ -12,7 +12,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { OutputUserDto, PERMISSION_NAMES, GENDERS } from '#types/api'; import { USER_GENDER } from '@exported/api/constants/genders'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -import { OutputBaseDTO } from '@modules/_mixin/dto/output.dto'; +import { OutputBaseDTO } from '@modules/base/dto/output.dto'; import { OutputFileDTO } from '@modules/files/dto/output.dto'; export class OutputBaseUserDTO extends OutputBaseDTO implements OutputBaseUserDto { diff --git a/src/modules/users/entities/user-visibility.entity.ts b/src/modules/users/entities/user-visibility.entity.ts index 8debbaaf..43355e69 100644 --- a/src/modules/users/entities/user-visibility.entity.ts +++ b/src/modules/users/entities/user-visibility.entity.ts @@ -1,6 +1,6 @@ import { Entity, Property, OneToOne } from '@mikro-orm/core'; -import { BaseEntity } from '@modules/_mixin/entities/base.entity'; +import { BaseEntity } from '@modules/base/entities/base.entity'; import { User } from './user.entity'; diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 73912330..1fc047ec 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -3,7 +3,7 @@ import type { email } from '#types'; import { Cascade, Collection, Entity, ManyToMany, ManyToOne, OneToMany, OneToOne, Property } from '@mikro-orm/core'; import { USER_GENDER } from '@exported/api/constants/genders'; -import { BaseEntity } from '@modules/_mixin/entities/base.entity'; +import { BaseEntity } from '@modules/base/entities/base.entity'; import { FileVisibilityGroup } from '@modules/files/entities/file-visibility.entity'; import { Log } from '@modules/logs/entities/log.entity'; import { Permission } from '@modules/permissions/entities/permission.entity'; diff --git a/src/modules/users/services/users-data.service.ts b/src/modules/users/services/users-data.service.ts index 6923ffdc..340ab2d7 100644 --- a/src/modules/users/services/users-data.service.ts +++ b/src/modules/users/services/users-data.service.ts @@ -9,9 +9,9 @@ import { I18nContext, I18nService } from 'nestjs-i18n'; import { z } from 'zod'; import { env } from '@env'; -import { generateRandomPassword, isStrongPassword } from '@modules/_mixin/decorators'; -import { OutputCreatedDTO, OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; -import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; +import { generateRandomPassword, isStrongPassword } from '@modules/base/decorators'; +import { OutputCreatedDTO, OutputMessageDTO } from '@modules/base/dto/output.dto'; +import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/base/http-errors'; import { InputRegisterUserAdminDTO, InputRegisterUserDTO } from '@modules/auth/dto/input.dto'; import { EmailsService } from '@modules/emails/emails.service'; import { OutputPermissionDTO } from '@modules/permissions/dto/output.dto'; diff --git a/src/modules/users/services/users-files.service.ts b/src/modules/users/services/users-files.service.ts index 851547fe..87444623 100644 --- a/src/modules/users/services/users-files.service.ts +++ b/src/modules/users/services/users-files.service.ts @@ -4,8 +4,8 @@ import { MikroORM, CreateRequestContext } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; import { env } from '@env'; -import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; -import { i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; +import { i18nNotFoundException, i18nUnauthorizedException } from '@modules/base/http-errors'; import { ImagesService } from '@modules/files/images.service'; import { UsersDataService } from './users-data.service'; diff --git a/tests/e2e/auth.e2e-spec.ts b/tests/e2e/auth.e2e-spec.ts index 301fe277..918ab856 100644 --- a/tests/e2e/auth.e2e-spec.ts +++ b/tests/e2e/auth.e2e-spec.ts @@ -3,10 +3,10 @@ import type { email } from '#types'; import { hashSync } from 'bcrypt'; import request from 'supertest'; -import { generateRandomPassword } from '@modules/_mixin/decorators'; -import { OutputCreatedDTO, OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; -import { i18nForbiddenException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; -import { i18nBadRequestException } from '@modules/_mixin/http-errors/bad-request'; +import { generateRandomPassword } from '@modules/base/decorators'; +import { OutputCreatedDTO, OutputMessageDTO } from '@modules/base/dto/output.dto'; +import { i18nForbiddenException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/base/http-errors'; +import { i18nBadRequestException } from '@modules/base/http-errors/bad-request'; import { InputRegisterUserDTO } from '@modules/auth/dto/input.dto'; import { User } from '@modules/users/entities/user.entity'; diff --git a/tests/e2e/logs.e2e-spec.ts b/tests/e2e/logs.e2e-spec.ts index 28f07e9f..9631d78d 100644 --- a/tests/e2e/logs.e2e-spec.ts +++ b/tests/e2e/logs.e2e-spec.ts @@ -1,7 +1,7 @@ import request from 'supertest'; -import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; -import { i18nBadRequestException } from '@modules/_mixin/http-errors'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; +import { i18nBadRequestException } from '@modules/base/http-errors'; import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { server } from '..'; diff --git a/tests/e2e/permissions.e2e-spec.ts b/tests/e2e/permissions.e2e-spec.ts index ed099e9d..14b95f5b 100644 --- a/tests/e2e/permissions.e2e-spec.ts +++ b/tests/e2e/permissions.e2e-spec.ts @@ -1,7 +1,7 @@ import request from 'supertest'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -import { i18nBadRequestException, i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/base/http-errors'; import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { Permission } from '@modules/permissions/entities/permission.entity'; diff --git a/tests/e2e/promotions.e2e-spec.ts b/tests/e2e/promotions.e2e-spec.ts index 5a9ee558..e5b51f6d 100644 --- a/tests/e2e/promotions.e2e-spec.ts +++ b/tests/e2e/promotions.e2e-spec.ts @@ -4,8 +4,8 @@ import { join } from 'path'; import request from 'supertest'; import { env } from '@env'; -import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; -import { i18nBadRequestException, i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/base/http-errors'; import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { PromotionPicture } from '@modules/promotions/entities/promotion-picture.entity'; import { Promotion } from '@modules/promotions/entities/promotion.entity'; diff --git a/tests/e2e/roles.e2e-spec.ts b/tests/e2e/roles.e2e-spec.ts index 5c3f5c5d..fae5595f 100644 --- a/tests/e2e/roles.e2e-spec.ts +++ b/tests/e2e/roles.e2e-spec.ts @@ -1,7 +1,7 @@ import request from 'supertest'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -import { i18nBadRequestException, i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/base/http-errors'; import { InputUpdateRoleUserDTO } from '@modules/roles/dto/input.dto'; import { Role } from '@modules/roles/entities/role.entity'; diff --git a/tests/e2e/users/users-data.e2e-spec.ts b/tests/e2e/users/users-data.e2e-spec.ts index 830262e4..3560ccb4 100644 --- a/tests/e2e/users/users-data.e2e-spec.ts +++ b/tests/e2e/users/users-data.e2e-spec.ts @@ -2,8 +2,8 @@ import type { email } from '#types'; import request from 'supertest'; -import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; -import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; +import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/base/http-errors'; import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { User } from '@modules/users/entities/user.entity'; diff --git a/tests/e2e/users/users-files.e2e-spec.ts b/tests/e2e/users/users-files.e2e-spec.ts index 45d9e9ab..20d18561 100644 --- a/tests/e2e/users/users-files.e2e-spec.ts +++ b/tests/e2e/users/users-files.e2e-spec.ts @@ -3,8 +3,8 @@ import { join } from 'path'; import request from 'supertest'; -import { OutputMessageDTO } from '@modules/_mixin/dto/output.dto'; -import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/_mixin/http-errors'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; +import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/base/http-errors'; import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { FileVisibilityGroup } from '@modules/files/entities/file-visibility.entity'; import { UserBanner } from '@modules/users/entities/user-banner.entity'; diff --git a/tests/index.ts b/tests/index.ts index 5705b07d..568d8daa 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -11,7 +11,7 @@ import { I18nContext, I18nService, I18nValidationExceptionFilter, I18nValidation import { AppModule } from '@app.module'; import { VALIDATION_PIPE_OPTIONS } from '@env'; -import { I18nHttpExceptionFilter } from '@modules/_mixin/http-errors'; +import { I18nHttpExceptionFilter } from '@modules/base/http-errors'; let module_fixture: TestingModule; let jwt: JwtService; diff --git a/tests/units/services/auth.test.ts b/tests/units/services/auth.test.ts index 853be2de..07f1c128 100644 --- a/tests/units/services/auth.test.ts +++ b/tests/units/services/auth.test.ts @@ -1,5 +1,5 @@ import { env } from '@env'; -import { i18nUnauthorizedException } from '@modules/_mixin/http-errors'; +import { i18nUnauthorizedException } from '@modules/base/http-errors'; import { AuthService } from '@modules/auth/auth.service'; import { module_fixture, jwt } from '../..'; diff --git a/tests/units/services/files.test.ts b/tests/units/services/files.test.ts index b123a70a..aa0e5bf0 100644 --- a/tests/units/services/files.test.ts +++ b/tests/units/services/files.test.ts @@ -1,4 +1,4 @@ -import { i18nBadRequestException, i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/base/http-errors'; import { FilesService } from '@modules/files/files.service'; import { PromotionPicture } from '@modules/promotions/entities/promotion-picture.entity'; import { Promotion } from '@modules/promotions/entities/promotion.entity'; diff --git a/tests/units/services/images.test.ts b/tests/units/services/images.test.ts index b6a3b311..d9a2bada 100644 --- a/tests/units/services/images.test.ts +++ b/tests/units/services/images.test.ts @@ -1,4 +1,4 @@ -import { i18nBadRequestException } from '@modules/_mixin/http-errors'; +import { i18nBadRequestException } from '@modules/base/http-errors'; import { ImagesService } from '@modules/files/images.service'; import { module_fixture } from '../..'; diff --git a/tests/units/services/users/users-data.test.ts b/tests/units/services/users/users-data.test.ts index e3e10b6f..e9e87094 100644 --- a/tests/units/services/users/users-data.test.ts +++ b/tests/units/services/users/users-data.test.ts @@ -1,6 +1,6 @@ import { hashSync } from 'bcrypt'; -import { i18nNotFoundException } from '@modules/_mixin/http-errors'; +import { i18nNotFoundException } from '@modules/base/http-errors'; import { Permission } from '@modules/permissions/entities/permission.entity'; import { Role } from '@modules/roles/entities/role.entity'; import { User } from '@modules/users/entities/user.entity'; diff --git a/tests/units/utils/password.test.ts b/tests/units/utils/password.test.ts index 3b015375..0fd20e76 100644 --- a/tests/units/utils/password.test.ts +++ b/tests/units/utils/password.test.ts @@ -1,4 +1,4 @@ -import { generateRandomPassword } from '@modules/_mixin/decorators/is-strong-pass.decorator'; +import { generateRandomPassword } from '@modules/base/decorators/is-strong-pass.decorator'; describe('Password (unit)', () => { describe('.generateRandomPassword()', () => { From 0dd66e37a9f16a0c7664a3d82cb6d115a75ca8b5 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Tue, 7 Nov 2023 18:47:21 +0100 Subject: [PATCH 09/15] refactor(dirs): renamed `src/../https-errors/base.ts` to `http-exception.ts` --- src/modules/base/http-errors/bad-request.ts | 2 +- src/modules/base/http-errors/forbidden.ts | 2 +- src/modules/base/http-errors/{base.ts => http-exception.ts} | 0 src/modules/base/http-errors/index.ts | 2 +- src/modules/base/http-errors/not-found.ts | 2 +- src/modules/base/http-errors/unauthorized.ts | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename src/modules/base/http-errors/{base.ts => http-exception.ts} (100%) diff --git a/src/modules/base/http-errors/bad-request.ts b/src/modules/base/http-errors/bad-request.ts index 8259ef48..1d152741 100644 --- a/src/modules/base/http-errors/bad-request.ts +++ b/src/modules/base/http-errors/bad-request.ts @@ -1,6 +1,6 @@ import type { HttpStatusNames } from '#types/api'; -import { I18nHttpException } from './base'; +import { I18nHttpException } from './http-exception'; export class i18nBadRequestException extends I18nHttpException { override statusCode: number = 400; diff --git a/src/modules/base/http-errors/forbidden.ts b/src/modules/base/http-errors/forbidden.ts index 8a8b95ac..94985385 100644 --- a/src/modules/base/http-errors/forbidden.ts +++ b/src/modules/base/http-errors/forbidden.ts @@ -1,6 +1,6 @@ import type { HttpStatusNames } from '#types/api'; -import { I18nHttpException } from './base'; +import { I18nHttpException } from './http-exception'; export class i18nForbiddenException extends I18nHttpException { override statusCode: number = 403; diff --git a/src/modules/base/http-errors/base.ts b/src/modules/base/http-errors/http-exception.ts similarity index 100% rename from src/modules/base/http-errors/base.ts rename to src/modules/base/http-errors/http-exception.ts diff --git a/src/modules/base/http-errors/index.ts b/src/modules/base/http-errors/index.ts index 64bfa36b..f8c3fb6e 100644 --- a/src/modules/base/http-errors/index.ts +++ b/src/modules/base/http-errors/index.ts @@ -2,4 +2,4 @@ export * from './forbidden'; export * from './not-found'; export * from './unauthorized'; export * from './bad-request'; -export * from './base'; +export * from './http-exception'; diff --git a/src/modules/base/http-errors/not-found.ts b/src/modules/base/http-errors/not-found.ts index 88b895df..f14e242b 100644 --- a/src/modules/base/http-errors/not-found.ts +++ b/src/modules/base/http-errors/not-found.ts @@ -1,6 +1,6 @@ import type { HttpStatusNames } from '#types/api'; -import { I18nHttpException } from './base'; +import { I18nHttpException } from './http-exception'; export class i18nNotFoundException extends I18nHttpException { override statusCode: number = 404; diff --git a/src/modules/base/http-errors/unauthorized.ts b/src/modules/base/http-errors/unauthorized.ts index 0aee36bd..1268a94e 100644 --- a/src/modules/base/http-errors/unauthorized.ts +++ b/src/modules/base/http-errors/unauthorized.ts @@ -1,6 +1,6 @@ import type { HttpStatusNames } from '#types/api'; -import { I18nHttpException } from './base'; +import { I18nHttpException } from './http-exception'; export class i18nUnauthorizedException extends I18nHttpException { override statusCode: number = 401; From 025ffa16a17b0d32ecb40fb227014583ce5139d6 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Tue, 7 Nov 2023 18:48:59 +0100 Subject: [PATCH 10/15] fix(lint): pnpm lint --- src/modules/logs/logs.controller.ts | 6 +++--- src/modules/permissions/permissions.controller.ts | 4 ++-- src/modules/promotions/promotions.controller.ts | 4 ++-- src/modules/roles/roles.controller.ts | 4 ++-- src/modules/users/controllers/users-data.controller.ts | 6 +++--- src/modules/users/controllers/users-files.controller.ts | 8 ++++---- src/modules/users/services/users-data.service.ts | 2 +- tests/e2e/auth.e2e-spec.ts | 2 +- tests/e2e/logs.e2e-spec.ts | 2 +- tests/e2e/permissions.e2e-spec.ts | 2 +- tests/e2e/promotions.e2e-spec.ts | 2 +- tests/e2e/users/users-data.e2e-spec.ts | 2 +- tests/e2e/users/users-files.e2e-spec.ts | 2 +- tests/units/services/auth.test.ts | 2 +- 14 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/modules/logs/logs.controller.ts b/src/modules/logs/logs.controller.ts index a6d47b80..026ca8ff 100644 --- a/src/modules/logs/logs.controller.ts +++ b/src/modules/logs/logs.controller.ts @@ -2,13 +2,13 @@ import { Controller, Delete, Get, Param, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiTags, ApiBearerAuth, ApiOkResponse, ApiParam, ApiOperation } from '@nestjs/swagger'; -import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; -import { InputIdParamDTO } from '@modules/base/dto/input.dto'; -import { OutputMessageDTO } from '@modules/base/dto/output.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { SelfOrPermissionGuard } from '@modules/auth/guards/self-or-perms.guard'; +import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/base/dto/input.dto'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; import { OutputLogDTO } from './dto/output.dto'; import { LogsService } from './logs.service'; diff --git a/src/modules/permissions/permissions.controller.ts b/src/modules/permissions/permissions.controller.ts index 44bf4a2b..56006b86 100644 --- a/src/modules/permissions/permissions.controller.ts +++ b/src/modules/permissions/permissions.controller.ts @@ -2,12 +2,12 @@ import { Body, Controller, Get, Param, Post, UseGuards, Patch } from '@nestjs/co import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; -import { InputIdParamDTO } from '@modules/base/dto/input.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { SelfOrPermissionGuard } from '@modules/auth/guards/self-or-perms.guard'; +import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/base/dto/input.dto'; import { InputUpdatePermissionDTO, InputCreatePermissionDTO } from './dto/input.dto'; import { OutputPermissionDTO } from './dto/output.dto'; diff --git a/src/modules/promotions/promotions.controller.ts b/src/modules/promotions/promotions.controller.ts index 09f4e4a4..57305f70 100644 --- a/src/modules/promotions/promotions.controller.ts +++ b/src/modules/promotions/promotions.controller.ts @@ -2,11 +2,11 @@ import { Controller, Delete, Get, Param, Post, Req, UploadedFile, UseGuards } fr import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; +import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; +import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; import { OutputMessageDTO } from '@modules/base/dto/output.dto'; import { i18nBadRequestException } from '@modules/base/http-errors'; -import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; -import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { ApiDownloadFile } from '@modules/files/decorators/download.decorator'; import { ApiUploadFile } from '@modules/files/decorators/upload.decorator'; import { FilesService } from '@modules/files/files.service'; diff --git a/src/modules/roles/roles.controller.ts b/src/modules/roles/roles.controller.ts index f724ae93..e0990747 100644 --- a/src/modules/roles/roles.controller.ts +++ b/src/modules/roles/roles.controller.ts @@ -2,10 +2,10 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@n import { AuthGuard } from '@nestjs/passport'; import { ApiTags, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiBody, ApiParam } from '@nestjs/swagger'; -import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; -import { InputIdParamDTO } from '@modules/base/dto/input.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; +import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/base/dto/input.dto'; import { InputUpdateRoleDTO, diff --git a/src/modules/users/controllers/users-data.controller.ts b/src/modules/users/controllers/users-data.controller.ts index e1356d66..f5acc6ac 100644 --- a/src/modules/users/controllers/users-data.controller.ts +++ b/src/modules/users/controllers/users-data.controller.ts @@ -2,9 +2,6 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Req, UseGuards } fro import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiBody, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; -import { InputIdParamDTO } from '@modules/base/dto/input.dto'; -import { OutputMessageDTO } from '@modules/base/dto/output.dto'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { GuardSelfParam } from '@modules/auth/decorators/self.decorator'; @@ -12,6 +9,9 @@ import { InputRegisterUsersAdminDTO } from '@modules/auth/dto/input.dto'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { SelfOrPermissionGuard } from '@modules/auth/guards/self-or-perms.guard'; import { SelfGuard } from '@modules/auth/guards/self.guard'; +import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/base/dto/input.dto'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; import { OutputPermissionDTO } from '@modules/permissions/dto/output.dto'; import { InputUpdateUserDTO, InputUpdateUserVisibilityDTO } from '../dto/input.dto'; diff --git a/src/modules/users/controllers/users-files.controller.ts b/src/modules/users/controllers/users-files.controller.ts index cc295aac..70d93720 100644 --- a/src/modules/users/controllers/users-files.controller.ts +++ b/src/modules/users/controllers/users-files.controller.ts @@ -2,14 +2,14 @@ import { Controller, Delete, Get, Param, Post, Req, UploadedFile, UseGuards } fr import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; -import { InputIdParamDTO } from '@modules/base/dto/input.dto'; -import { OutputMessageDTO } from '@modules/base/dto/output.dto'; -import { i18nBadRequestException } from '@modules/base/http-errors'; import { GuardPermissions } from '@modules/auth/decorators/permissions.decorator'; import { GuardSelfOrPermissions } from '@modules/auth/decorators/self-or-perms.decorator'; import { PermissionGuard } from '@modules/auth/guards/permission.guard'; import { SelfOrPermissionGuard } from '@modules/auth/guards/self-or-perms.guard'; +import { ApiNotOkResponses } from '@modules/base/decorators/api-not-ok.decorator'; +import { InputIdParamDTO } from '@modules/base/dto/input.dto'; +import { OutputMessageDTO } from '@modules/base/dto/output.dto'; +import { i18nBadRequestException } from '@modules/base/http-errors'; import { ApiDownloadFile } from '@modules/files/decorators/download.decorator'; import { ApiUploadFile } from '@modules/files/decorators/upload.decorator'; import { FilesService } from '@modules/files/files.service'; diff --git a/src/modules/users/services/users-data.service.ts b/src/modules/users/services/users-data.service.ts index 340ab2d7..87305809 100644 --- a/src/modules/users/services/users-data.service.ts +++ b/src/modules/users/services/users-data.service.ts @@ -9,10 +9,10 @@ import { I18nContext, I18nService } from 'nestjs-i18n'; import { z } from 'zod'; import { env } from '@env'; +import { InputRegisterUserAdminDTO, InputRegisterUserDTO } from '@modules/auth/dto/input.dto'; import { generateRandomPassword, isStrongPassword } from '@modules/base/decorators'; import { OutputCreatedDTO, OutputMessageDTO } from '@modules/base/dto/output.dto'; import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/base/http-errors'; -import { InputRegisterUserAdminDTO, InputRegisterUserDTO } from '@modules/auth/dto/input.dto'; import { EmailsService } from '@modules/emails/emails.service'; import { OutputPermissionDTO } from '@modules/permissions/dto/output.dto'; import { RoleExpiration } from '@modules/roles/entities/role-expiration.entity'; diff --git a/tests/e2e/auth.e2e-spec.ts b/tests/e2e/auth.e2e-spec.ts index 918ab856..30ba1684 100644 --- a/tests/e2e/auth.e2e-spec.ts +++ b/tests/e2e/auth.e2e-spec.ts @@ -3,11 +3,11 @@ import type { email } from '#types'; import { hashSync } from 'bcrypt'; import request from 'supertest'; +import { InputRegisterUserDTO } from '@modules/auth/dto/input.dto'; import { generateRandomPassword } from '@modules/base/decorators'; import { OutputCreatedDTO, OutputMessageDTO } from '@modules/base/dto/output.dto'; import { i18nForbiddenException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/base/http-errors'; import { i18nBadRequestException } from '@modules/base/http-errors/bad-request'; -import { InputRegisterUserDTO } from '@modules/auth/dto/input.dto'; import { User } from '@modules/users/entities/user.entity'; import { orm, server } from '..'; diff --git a/tests/e2e/logs.e2e-spec.ts b/tests/e2e/logs.e2e-spec.ts index 9631d78d..ca1f5c13 100644 --- a/tests/e2e/logs.e2e-spec.ts +++ b/tests/e2e/logs.e2e-spec.ts @@ -1,8 +1,8 @@ import request from 'supertest'; +import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { OutputMessageDTO } from '@modules/base/dto/output.dto'; import { i18nBadRequestException } from '@modules/base/http-errors'; -import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { server } from '..'; diff --git a/tests/e2e/permissions.e2e-spec.ts b/tests/e2e/permissions.e2e-spec.ts index 14b95f5b..ecdeaf60 100644 --- a/tests/e2e/permissions.e2e-spec.ts +++ b/tests/e2e/permissions.e2e-spec.ts @@ -1,8 +1,8 @@ import request from 'supertest'; import { PERMISSIONS_NAMES } from '@exported/api/constants/perms'; -import { i18nBadRequestException, i18nNotFoundException } from '@modules/base/http-errors'; import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; +import { i18nBadRequestException, i18nNotFoundException } from '@modules/base/http-errors'; import { Permission } from '@modules/permissions/entities/permission.entity'; import { server, orm } from '..'; diff --git a/tests/e2e/promotions.e2e-spec.ts b/tests/e2e/promotions.e2e-spec.ts index e5b51f6d..a26673d0 100644 --- a/tests/e2e/promotions.e2e-spec.ts +++ b/tests/e2e/promotions.e2e-spec.ts @@ -4,9 +4,9 @@ import { join } from 'path'; import request from 'supertest'; import { env } from '@env'; +import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { OutputMessageDTO } from '@modules/base/dto/output.dto'; import { i18nBadRequestException, i18nNotFoundException } from '@modules/base/http-errors'; -import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { PromotionPicture } from '@modules/promotions/entities/promotion-picture.entity'; import { Promotion } from '@modules/promotions/entities/promotion.entity'; diff --git a/tests/e2e/users/users-data.e2e-spec.ts b/tests/e2e/users/users-data.e2e-spec.ts index 3560ccb4..5f8b67f6 100644 --- a/tests/e2e/users/users-data.e2e-spec.ts +++ b/tests/e2e/users/users-data.e2e-spec.ts @@ -2,9 +2,9 @@ import type { email } from '#types'; import request from 'supertest'; +import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { OutputMessageDTO } from '@modules/base/dto/output.dto'; import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/base/http-errors'; -import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { User } from '@modules/users/entities/user.entity'; import { orm, server } from '../..'; diff --git a/tests/e2e/users/users-files.e2e-spec.ts b/tests/e2e/users/users-files.e2e-spec.ts index 20d18561..0566beca 100644 --- a/tests/e2e/users/users-files.e2e-spec.ts +++ b/tests/e2e/users/users-files.e2e-spec.ts @@ -3,9 +3,9 @@ import { join } from 'path'; import request from 'supertest'; +import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { OutputMessageDTO } from '@modules/base/dto/output.dto'; import { i18nBadRequestException, i18nNotFoundException, i18nUnauthorizedException } from '@modules/base/http-errors'; -import { OutputTokenDTO } from '@modules/auth/dto/output.dto'; import { FileVisibilityGroup } from '@modules/files/entities/file-visibility.entity'; import { UserBanner } from '@modules/users/entities/user-banner.entity'; import { UserPicture } from '@modules/users/entities/user-picture.entity'; diff --git a/tests/units/services/auth.test.ts b/tests/units/services/auth.test.ts index 07f1c128..4045b41e 100644 --- a/tests/units/services/auth.test.ts +++ b/tests/units/services/auth.test.ts @@ -1,6 +1,6 @@ import { env } from '@env'; -import { i18nUnauthorizedException } from '@modules/base/http-errors'; import { AuthService } from '@modules/auth/auth.service'; +import { i18nUnauthorizedException } from '@modules/base/http-errors'; import { module_fixture, jwt } from '../..'; From f2fecd439ccbd7c1b6ba4c02578263ce9d58ca3c Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Wed, 8 Nov 2023 17:24:53 +0100 Subject: [PATCH 11/15] chore: clean up --- src/modules/base/http-errors/http-exception.ts | 17 +++++++---------- tests/index.ts | 5 +---- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/modules/base/http-errors/http-exception.ts b/src/modules/base/http-errors/http-exception.ts index 7437ca8d..bf7653e6 100644 --- a/src/modules/base/http-errors/http-exception.ts +++ b/src/modules/base/http-errors/http-exception.ts @@ -1,9 +1,8 @@ import type { I18nTranslations, OutputErrorResponseDto } from '#types/api'; import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; -import { PathImpl2 } from '@nestjs/config'; import { Response } from 'express'; -import { I18nContext, TranslateOptions } from 'nestjs-i18n'; +import { I18nContext, Path, TranslateOptions } from 'nestjs-i18n'; export abstract class I18nHttpException extends Error implements OutputErrorResponseDto { /** @@ -11,7 +10,7 @@ export abstract class I18nHttpException extends Error implements OutputErrorResp * @param key A key from the translation file, e.g. 'validations.user.success.registered' * @param args An optional object containing the args to pass to the translation function */ - constructor(key: PathImpl2, args?: TranslateOptions['args']); + constructor(key: Path, args?: TranslateOptions['args']); /** * Pass multiple keys (with or without optional args) to be translated and concatenated @@ -22,9 +21,7 @@ export abstract class I18nHttpException extends Error implements OutputErrorResp * Or specify args for some keys: * @example new OutputMessageDTO(['key.foo.bar', { key: 'another.key', args: { test: 'value' } }]) */ - constructor( - keys: ({ key: PathImpl2; args?: TranslateOptions['args'] } | PathImpl2)[], - ); + constructor(keys: ({ key: Path; args?: TranslateOptions['args'] } | Path)[]); /** * @remark do not use this constructor directly, use the overloads instead @@ -39,8 +36,8 @@ export abstract class I18nHttpException extends Error implements OutputErrorResp // Handle the case where multiple keys (with or without optional args) are passed if (val[0].length > 0) { const keys = val[0] as ( - | { key: PathImpl2; args?: TranslateOptions['args'] } - | PathImpl2 + | { key: Path; args?: TranslateOptions['args'] } + | Path )[]; for (const key_or_key_with_args of keys) { @@ -52,12 +49,12 @@ export abstract class I18nHttpException extends Error implements OutputErrorResp } } else if (val.length === 1 && !Array.isArray(val[0])) { // Handle the case where a single key is passed without args - const key = val[0] as PathImpl2; + const key = val[0] as Path; this.errors.push(ctx.t(key)); } else if (val.length === 2) { // Handle the case where a single key and optional args are passed - const key = val[0] as PathImpl2; + const key = val[0] as Path; const args = val[1] as TranslateOptions['args']; this.errors.push(ctx.t(key, { args })); diff --git a/tests/index.ts b/tests/index.ts index 568d8daa..e65c81e3 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -54,7 +54,4 @@ afterAll(async () => { server.close(); }); -/** @deprecated */ -const t = {}; - -export { module_fixture, server, orm, t, jwt }; +export { module_fixture, server, orm, jwt }; From 4657d1e3792c90ebe2ea5821e7707cf55340076a Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Wed, 8 Nov 2023 23:17:35 +0100 Subject: [PATCH 12/15] fix(types): fix typings --- package.json | 1 + pnpm-lock.yaml | 336 ++++++++++++++++++++++++---- src/exported | 2 +- src/modules/files/dto/output.dto.ts | 4 +- src/modules/users/dto/output.dto.ts | 2 +- 5 files changed, 298 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index 84e18cb5..07c397c6 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "eslint-plugin-import": "^2.29.0", "eslint-plugin-prettier": "^4.2.1", "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "jest-extended": "^4.0.2", "prettier": "^2.8.8", "source-map-support": "^0.5.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb654b66..ece91128 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ dependencies: file-type: specifier: ^16.5.4 version: 16.5.4 + jest-environment-jsdom: + specifier: ^29.7.0 + version: 29.7.0 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -261,7 +264,6 @@ packages: dependencies: '@babel/highlight': 7.22.20 chalk: 2.4.2 - dev: true /@babel/compat-data@7.23.2: resolution: {integrity: sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==} @@ -380,7 +382,6 @@ packages: /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-option@7.22.15: resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} @@ -405,7 +406,6 @@ packages: '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true /@babel/parser@7.23.0: resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} @@ -759,7 +759,6 @@ packages: '@jest/types': 29.6.3 '@types/node': 20.5.7 jest-mock: 29.7.0 - dev: true /@jest/expect-utils@29.7.0: resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} @@ -788,7 +787,6 @@ packages: jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 - dev: true /@jest/globals@29.7.0: resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} @@ -844,7 +842,6 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@sinclair/typebox': 0.27.8 - dev: true /@jest/source-map@29.6.3: resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} @@ -908,7 +905,6 @@ packages: '@types/node': 20.5.7 '@types/yargs': 17.0.29 chalk: 4.1.2 - dev: true /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} @@ -1511,24 +1507,26 @@ packages: /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - dev: true /@sinonjs/commons@3.0.0: resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} dependencies: type-detect: 4.0.8 - dev: true /@sinonjs/fake-timers@10.3.0: resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} dependencies: '@sinonjs/commons': 3.0.0 - dev: true /@tokenizer/token@0.3.0: resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} dev: false + /@tootallnate/once@2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: false + /@ts-morph/common@0.21.0: resolution: {integrity: sha512-ES110Mmne5Vi4ypUKrtVQfXFDtCsDXiUiGxF6ILVlE90dDD4fdpC1LSjydl/ml7xJWKSDZwUYD2zkOePMSrPBA==} dependencies: @@ -1658,19 +1656,16 @@ packages: /@types/istanbul-lib-coverage@2.0.5: resolution: {integrity: sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==} - dev: true /@types/istanbul-lib-report@3.0.2: resolution: {integrity: sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==} dependencies: '@types/istanbul-lib-coverage': 2.0.5 - dev: true /@types/istanbul-reports@3.0.3: resolution: {integrity: sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==} dependencies: '@types/istanbul-lib-report': 3.0.2 - dev: true /@types/jest@29.5.2: resolution: {integrity: sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==} @@ -1679,6 +1674,14 @@ packages: pretty-format: 29.7.0 dev: true + /@types/jsdom@20.0.1: + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + dependencies: + '@types/node': 20.5.7 + '@types/tough-cookie': 4.0.5 + parse5: 7.1.2 + dev: false + /@types/json-schema@7.0.14: resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==} dev: true @@ -1776,7 +1779,6 @@ packages: /@types/stack-utils@2.0.2: resolution: {integrity: sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==} - dev: true /@types/superagent@4.1.20: resolution: {integrity: sha512-GfpwJgYSr3yO+nArFkmyqv3i0vZavyEG5xPd/o95RwpKYpsOKJYI5XLdxLpdRbZI3YiGKKdIOFIf/jlP7A0Jxg==} @@ -1791,18 +1793,20 @@ packages: '@types/superagent': 4.1.20 dev: true + /@types/tough-cookie@4.0.5: + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + dev: false + /@types/validator@13.11.5: resolution: {integrity: sha512-xW4qsT4UIYILu+7ZrBnfQdBYniZrMLYYK3wN9M/NdeIHgBN5pZI2/8Q7UfdWIcr5RLJv/OGENsx91JIpUUoC7Q==} /@types/yargs-parser@21.0.2: resolution: {integrity: sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==} - dev: true /@types/yargs@17.0.29: resolution: {integrity: sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==} dependencies: '@types/yargs-parser': 21.0.2 - dev: true /@typescript-eslint/eslint-plugin@6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.52.0)(typescript@5.2.2): resolution: {integrity: sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==} @@ -2053,6 +2057,10 @@ packages: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} dev: true + /abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + dev: false + /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: false @@ -2068,6 +2076,13 @@ packages: mime-types: 2.1.35 negotiator: 0.6.3 + /acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + dependencies: + acorn: 8.11.2 + acorn-walk: 8.3.0 + dev: false + /acorn-import-assertions@1.9.0(acorn@8.11.2): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} peerDependencies: @@ -2099,7 +2114,6 @@ packages: /acorn-walk@8.3.0: resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} engines: {node: '>=0.4.0'} - dev: true /acorn@8.11.2: resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} @@ -2177,7 +2191,6 @@ packages: engines: {node: '>=4'} dependencies: color-convert: 1.9.3 - dev: true /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -2188,7 +2201,6 @@ packages: /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - dev: true /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} @@ -2308,7 +2320,6 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: true /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} @@ -2560,7 +2571,6 @@ packages: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -2609,7 +2619,6 @@ packages: /ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - dev: true /cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} @@ -2681,7 +2690,6 @@ packages: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: color-name: 1.1.3 - dev: true /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -2691,7 +2699,6 @@ packages: /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -2730,7 +2737,6 @@ packages: engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 - dev: true /commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} @@ -2870,6 +2876,30 @@ packages: which: 2.0.2 dev: true + /cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: false + + /cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: false + + /cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + dependencies: + cssom: 0.3.8 + dev: false + + /data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + dev: false + /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -2902,6 +2932,10 @@ packages: dependencies: ms: 2.1.2 + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: false + /decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -2958,7 +2992,6 @@ packages: /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - dev: true /delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} @@ -3019,6 +3052,13 @@ packages: esutils: 2.0.3 dev: true + /domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + dependencies: + webidl-conversions: 7.0.0 + dev: false + /dotenv-expand@10.0.0: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} @@ -3074,6 +3114,11 @@ packages: tapable: 2.2.1 dev: true + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: false + /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -3163,18 +3208,28 @@ packages: /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - dev: true /escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} - dev: true /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} dev: true + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: false + /eslint-config-prettier@9.0.0(eslint@8.52.0): resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} hasBin: true @@ -3385,7 +3440,6 @@ packages: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - dev: true /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} @@ -3409,12 +3463,10 @@ packages: /estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} - dev: true /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - dev: true /etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} @@ -3689,7 +3741,6 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: true /formidable@2.1.2: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} @@ -3947,7 +3998,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -3993,6 +4043,13 @@ packages: engines: {node: '>=8'} dev: true + /html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: false + /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true @@ -4007,6 +4064,17 @@ packages: statuses: 2.0.1 toidentifier: 1.0.1 + /http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -4033,6 +4101,13 @@ packages: dependencies: safer-buffer: 2.1.2 + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -4238,6 +4313,10 @@ packages: engines: {node: '>=8'} dev: true + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: false + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -4507,6 +4586,29 @@ packages: pretty-format: 29.7.0 dev: true + /jest-environment-jsdom@29.7.0: + resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/jsdom': 20.0.1 + '@types/node': 20.5.7 + jest-mock: 29.7.0 + jest-util: 29.7.0 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /jest-environment-node@29.7.0: resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4588,7 +4690,6 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 - dev: true /jest-mock@29.7.0: resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} @@ -4597,7 +4698,6 @@ packages: '@jest/types': 29.6.3 '@types/node': 20.5.7 jest-util: 29.7.0 - dev: true /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} @@ -4738,7 +4838,6 @@ packages: ci-info: 3.9.0 graceful-fs: 4.2.11 picomatch: 2.3.1 - dev: true /jest-validate@29.7.0: resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} @@ -4808,7 +4907,6 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} @@ -4824,6 +4922,47 @@ packages: dependencies: argparse: 2.0.1 + /jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.11.2 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.7 + parse5: 7.1.2 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.3 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.14.2 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -5412,6 +5551,10 @@ packages: set-blocking: 2.0.0 dev: false + /nwsapi@2.2.7: + resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -5578,6 +5721,12 @@ packages: lines-and-columns: 1.2.4 dev: true + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: false + /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -5822,7 +5971,6 @@ packages: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.2.0 - dev: true /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -5842,6 +5990,10 @@ packages: forwarded: 0.2.0 ipaddr.js: 1.9.1 + /psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: false + /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -5851,7 +6003,6 @@ packages: /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - dev: true /pure-rand@6.0.4: resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} @@ -5870,6 +6021,10 @@ packages: side-channel: 1.0.4 dev: true + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -5917,7 +6072,6 @@ packages: /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - dev: true /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -5991,6 +6145,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false + /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -6092,6 +6250,13 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: false + /schema-utils@3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} @@ -6269,7 +6434,6 @@ packages: /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true /source-map@0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} @@ -6294,7 +6458,6 @@ packages: engines: {node: '>=10'} dependencies: escape-string-regexp: 2.0.0 - dev: true /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} @@ -6458,7 +6621,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -6486,6 +6648,10 @@ packages: engines: {node: '>=0.10'} dev: true + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: false + /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -6635,9 +6801,26 @@ packages: ieee754: 1.2.1 dev: false + /tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: false + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + /tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.3.1 + dev: false + /tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -6785,7 +6968,6 @@ packages: /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} - dev: true /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} @@ -6892,6 +7074,11 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: false + /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -6917,6 +7104,13 @@ packages: punycode: 2.3.1 dev: true + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -6955,6 +7149,13 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + /w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + dependencies: + xml-name-validator: 4.0.0 + dev: false + /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: @@ -6978,6 +7179,11 @@ packages: /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: false + /webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} @@ -7028,6 +7234,26 @@ packages: - uglify-js dev: true + /whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: false + + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: false + + /whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: false + /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: @@ -7113,6 +7339,28 @@ packages: signal-exit: 3.0.7 dev: true + /ws@8.14.2: + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: false + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: false + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/src/exported b/src/exported index 6c36d852..d8e23ec9 160000 --- a/src/exported +++ b/src/exported @@ -1 +1 @@ -Subproject commit 6c36d852855799d808611bb094d303bc21dc5206 +Subproject commit d8e23ec96cab2a56ee7086fca4256f7f91959fb2 diff --git a/src/modules/files/dto/output.dto.ts b/src/modules/files/dto/output.dto.ts index 0483b657..e8d21571 100644 --- a/src/modules/files/dto/output.dto.ts +++ b/src/modules/files/dto/output.dto.ts @@ -2,6 +2,8 @@ import type { OutputFileDto, OutputFileVisibilityGroupDto } from '#types/api'; import { ApiProperty } from '@nestjs/swagger'; +import { OutputBaseDTO } from '@modules/base/dto/output.dto'; + export class OutputFileDTO implements OutputFileDto { @ApiProperty() id: number; @@ -31,7 +33,7 @@ export class OutputFileDTO implements OutputFileDto { description?: string; } -export class OutputFileVisibilityGroupDTO implements OutputFileVisibilityGroupDto { +export class OutputFileVisibilityGroupDTO extends OutputBaseDTO implements OutputFileVisibilityGroupDto { @ApiProperty() name: Uppercase; diff --git a/src/modules/users/dto/output.dto.ts b/src/modules/users/dto/output.dto.ts index eea6403d..6805d126 100644 --- a/src/modules/users/dto/output.dto.ts +++ b/src/modules/users/dto/output.dto.ts @@ -90,7 +90,7 @@ export class OutputUserDTO extends OutputBaseDTO implements OutputUserDto { subscribed: boolean; // TODO: (KEY: 2) Make a PR to implement subscriptions in the API @ApiProperty({ required: false }) - secondary_email?: string; + secondary_email?: email; @ApiProperty({ required: false }) phone?: string; From d5468b8e1c06ef7cae743249fe259264dce62997 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Wed, 8 Nov 2023 23:22:46 +0100 Subject: [PATCH 13/15] fix(eslint): fix warning --- src/exported | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exported b/src/exported index d8e23ec9..fda70b8d 160000 --- a/src/exported +++ b/src/exported @@ -1 +1 @@ -Subproject commit d8e23ec96cab2a56ee7086fca4256f7f91959fb2 +Subproject commit fda70b8df071b4a5671c8181824355731b5f845b From ca446a4c6e145eefff5587b666c360eeaaff57e1 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Wed, 8 Nov 2023 23:27:08 +0100 Subject: [PATCH 14/15] Update exported --- src/exported | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exported b/src/exported index fda70b8d..d9037c6f 160000 --- a/src/exported +++ b/src/exported @@ -1 +1 @@ -Subproject commit fda70b8df071b4a5671c8181824355731b5f845b +Subproject commit d9037c6f220fec7822578ff6719d5d8793c50671 From bfdd1ce15274761075e39984d51a633eadc8af3c Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Wed, 8 Nov 2023 23:29:13 +0100 Subject: [PATCH 15/15] fix(pnpm): fix corrupted lock file --- pnpm-lock.yaml | 128 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 43 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ece91128..76e68234 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,9 +74,6 @@ dependencies: file-type: specifier: ^16.5.4 version: 16.5.4 - jest-environment-jsdom: - specifier: ^29.7.0 - version: 29.7.0 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -169,6 +166,9 @@ devDependencies: jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.5.7)(ts-node@10.9.1) + jest-environment-jsdom: + specifier: ^29.7.0 + version: 29.7.0 jest-extended: specifier: ^4.0.2 version: 4.0.2(jest@29.7.0) @@ -264,6 +264,7 @@ packages: dependencies: '@babel/highlight': 7.22.20 chalk: 2.4.2 + dev: true /@babel/compat-data@7.23.2: resolution: {integrity: sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==} @@ -382,6 +383,7 @@ packages: /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} + dev: true /@babel/helper-validator-option@7.22.15: resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} @@ -406,6 +408,7 @@ packages: '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 + dev: true /@babel/parser@7.23.0: resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} @@ -759,6 +762,7 @@ packages: '@jest/types': 29.6.3 '@types/node': 20.5.7 jest-mock: 29.7.0 + dev: true /@jest/expect-utils@29.7.0: resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} @@ -787,6 +791,7 @@ packages: jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 + dev: true /@jest/globals@29.7.0: resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} @@ -842,6 +847,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@sinclair/typebox': 0.27.8 + dev: true /@jest/source-map@29.6.3: resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} @@ -905,6 +911,7 @@ packages: '@types/node': 20.5.7 '@types/yargs': 17.0.29 chalk: 4.1.2 + dev: true /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} @@ -1507,16 +1514,19 @@ packages: /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true /@sinonjs/commons@3.0.0: resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} dependencies: type-detect: 4.0.8 + dev: true /@sinonjs/fake-timers@10.3.0: resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} dependencies: '@sinonjs/commons': 3.0.0 + dev: true /@tokenizer/token@0.3.0: resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -1525,7 +1535,7 @@ packages: /@tootallnate/once@2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} - dev: false + dev: true /@ts-morph/common@0.21.0: resolution: {integrity: sha512-ES110Mmne5Vi4ypUKrtVQfXFDtCsDXiUiGxF6ILVlE90dDD4fdpC1LSjydl/ml7xJWKSDZwUYD2zkOePMSrPBA==} @@ -1656,16 +1666,19 @@ packages: /@types/istanbul-lib-coverage@2.0.5: resolution: {integrity: sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==} + dev: true /@types/istanbul-lib-report@3.0.2: resolution: {integrity: sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==} dependencies: '@types/istanbul-lib-coverage': 2.0.5 + dev: true /@types/istanbul-reports@3.0.3: resolution: {integrity: sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==} dependencies: '@types/istanbul-lib-report': 3.0.2 + dev: true /@types/jest@29.5.2: resolution: {integrity: sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==} @@ -1680,7 +1693,7 @@ packages: '@types/node': 20.5.7 '@types/tough-cookie': 4.0.5 parse5: 7.1.2 - dev: false + dev: true /@types/json-schema@7.0.14: resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==} @@ -1779,6 +1792,7 @@ packages: /@types/stack-utils@2.0.2: resolution: {integrity: sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==} + dev: true /@types/superagent@4.1.20: resolution: {integrity: sha512-GfpwJgYSr3yO+nArFkmyqv3i0vZavyEG5xPd/o95RwpKYpsOKJYI5XLdxLpdRbZI3YiGKKdIOFIf/jlP7A0Jxg==} @@ -1795,18 +1809,20 @@ packages: /@types/tough-cookie@4.0.5: resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} - dev: false + dev: true /@types/validator@13.11.5: resolution: {integrity: sha512-xW4qsT4UIYILu+7ZrBnfQdBYniZrMLYYK3wN9M/NdeIHgBN5pZI2/8Q7UfdWIcr5RLJv/OGENsx91JIpUUoC7Q==} /@types/yargs-parser@21.0.2: resolution: {integrity: sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==} + dev: true /@types/yargs@17.0.29: resolution: {integrity: sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==} dependencies: '@types/yargs-parser': 21.0.2 + dev: true /@typescript-eslint/eslint-plugin@6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.52.0)(typescript@5.2.2): resolution: {integrity: sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==} @@ -2059,7 +2075,7 @@ packages: /abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} - dev: false + dev: true /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -2081,7 +2097,7 @@ packages: dependencies: acorn: 8.11.2 acorn-walk: 8.3.0 - dev: false + dev: true /acorn-import-assertions@1.9.0(acorn@8.11.2): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} @@ -2114,6 +2130,7 @@ packages: /acorn-walk@8.3.0: resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} engines: {node: '>=0.4.0'} + dev: true /acorn@8.11.2: resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} @@ -2127,7 +2144,6 @@ packages: debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: false /ajv-formats@2.1.1(ajv@8.12.0): resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} @@ -2191,6 +2207,7 @@ packages: engines: {node: '>=4'} dependencies: color-convert: 1.9.3 + dev: true /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -2201,6 +2218,7 @@ packages: /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + dev: true /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} @@ -2320,6 +2338,7 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} @@ -2571,6 +2590,7 @@ packages: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 + dev: true /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -2619,6 +2639,7 @@ packages: /ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + dev: true /cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} @@ -2690,6 +2711,7 @@ packages: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: color-name: 1.1.3 + dev: true /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -2699,6 +2721,7 @@ packages: /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -2737,6 +2760,7 @@ packages: engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 + dev: true /commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} @@ -2878,18 +2902,18 @@ packages: /cssom@0.3.8: resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} - dev: false + dev: true /cssom@0.5.0: resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} - dev: false + dev: true /cssstyle@2.3.0: resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} engines: {node: '>=8'} dependencies: cssom: 0.3.8 - dev: false + dev: true /data-urls@3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} @@ -2898,7 +2922,7 @@ packages: abab: 2.0.6 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - dev: false + dev: true /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} @@ -2934,7 +2958,7 @@ packages: /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - dev: false + dev: true /decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} @@ -2992,6 +3016,7 @@ packages: /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + dev: true /delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} @@ -3057,7 +3082,7 @@ packages: engines: {node: '>=12'} dependencies: webidl-conversions: 7.0.0 - dev: false + dev: true /dotenv-expand@10.0.0: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} @@ -3117,7 +3142,7 @@ packages: /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - dev: false + dev: true /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -3208,10 +3233,12 @@ packages: /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} + dev: true /escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + dev: true /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} @@ -3228,7 +3255,7 @@ packages: esutils: 2.0.3 optionalDependencies: source-map: 0.6.1 - dev: false + dev: true /eslint-config-prettier@9.0.0(eslint@8.52.0): resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} @@ -3440,6 +3467,7 @@ packages: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + dev: true /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} @@ -3463,10 +3491,12 @@ packages: /estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + dev: true /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + dev: true /etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} @@ -3741,6 +3771,7 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 + dev: true /formidable@2.1.2: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} @@ -3998,6 +4029,7 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -4048,7 +4080,7 @@ packages: engines: {node: '>=12'} dependencies: whatwg-encoding: 2.0.0 - dev: false + dev: true /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -4073,7 +4105,7 @@ packages: debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: false + dev: true /https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} @@ -4083,7 +4115,6 @@ packages: debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: false /human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} @@ -4106,7 +4137,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: safer-buffer: 2.1.2 - dev: false + dev: true /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -4315,7 +4346,7 @@ packages: /is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - dev: false + dev: true /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} @@ -4607,7 +4638,7 @@ packages: - bufferutil - supports-color - utf-8-validate - dev: false + dev: true /jest-environment-node@29.7.0: resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} @@ -4690,6 +4721,7 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 + dev: true /jest-mock@29.7.0: resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} @@ -4698,6 +4730,7 @@ packages: '@jest/types': 29.6.3 '@types/node': 20.5.7 jest-util: 29.7.0 + dev: true /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} @@ -4838,6 +4871,7 @@ packages: ci-info: 3.9.0 graceful-fs: 4.2.11 picomatch: 2.3.1 + dev: true /jest-validate@29.7.0: resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} @@ -4907,6 +4941,7 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} @@ -4961,7 +4996,7 @@ packages: - bufferutil - supports-color - utf-8-validate - dev: false + dev: true /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} @@ -5553,7 +5588,7 @@ packages: /nwsapi@2.2.7: resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} - dev: false + dev: true /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -5725,7 +5760,7 @@ packages: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: entities: 4.5.0 - dev: false + dev: true /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} @@ -5971,6 +6006,7 @@ packages: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.2.0 + dev: true /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -5992,7 +6028,7 @@ packages: /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} - dev: false + dev: true /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} @@ -6003,6 +6039,7 @@ packages: /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + dev: true /pure-rand@6.0.4: resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} @@ -6023,7 +6060,7 @@ packages: /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - dev: false + dev: true /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -6072,6 +6109,7 @@ packages: /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: true /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -6147,7 +6185,7 @@ packages: /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - dev: false + dev: true /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} @@ -6255,7 +6293,7 @@ packages: engines: {node: '>=v12.22.7'} dependencies: xmlchars: 2.2.0 - dev: false + dev: true /schema-utils@3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} @@ -6434,6 +6472,7 @@ packages: /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + dev: true /source-map@0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} @@ -6458,6 +6497,7 @@ packages: engines: {node: '>=10'} dependencies: escape-string-regexp: 2.0.0 + dev: true /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} @@ -6621,6 +6661,7 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 + dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -6650,7 +6691,7 @@ packages: /symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - dev: false + dev: true /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} @@ -6809,7 +6850,7 @@ packages: punycode: 2.3.1 universalify: 0.2.0 url-parse: 1.5.10 - dev: false + dev: true /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -6819,7 +6860,7 @@ packages: engines: {node: '>=12'} dependencies: punycode: 2.3.1 - dev: false + dev: true /tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} @@ -6968,6 +7009,7 @@ packages: /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + dev: true /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} @@ -7077,7 +7119,7 @@ packages: /universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} - dev: false + dev: true /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} @@ -7109,7 +7151,7 @@ packages: dependencies: querystringify: 2.2.0 requires-port: 1.0.0 - dev: false + dev: true /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -7154,7 +7196,7 @@ packages: engines: {node: '>=14'} dependencies: xml-name-validator: 4.0.0 - dev: false + dev: true /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -7182,7 +7224,7 @@ packages: /webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - dev: false + dev: true /webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} @@ -7239,12 +7281,12 @@ packages: engines: {node: '>=12'} dependencies: iconv-lite: 0.6.3 - dev: false + dev: true /whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} - dev: false + dev: true /whatwg-url@11.0.0: resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} @@ -7252,7 +7294,7 @@ packages: dependencies: tr46: 3.0.0 webidl-conversions: 7.0.0 - dev: false + dev: true /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -7350,16 +7392,16 @@ packages: optional: true utf-8-validate: optional: true - dev: false + dev: true /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} - dev: false + dev: true /xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - dev: false + dev: true /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}