From 1fe7c202f599ce6e19f2ac1b439baf4d7b834a4f Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Sat, 22 Mar 2025 01:22:57 -0400 Subject: [PATCH 1/4] add changes --- src/user/dto/profile.dto.ts | 3 ++ src/user/dto/user-create.dto.ts | 5 +++ src/user/dto/user-list.dto.ts | 47 ++++++++++++++++++++++ src/user/dto/user.dto.ts | 10 ++++- src/user/entities/user.entity.ts | 4 ++ src/user/user.controller.ts | 69 +++++++++++++++++++++++++++++++- src/user/user.service.ts | 24 +++++++++++ 7 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 src/user/dto/user-create.dto.ts create mode 100644 src/user/dto/user-list.dto.ts diff --git a/src/user/dto/profile.dto.ts b/src/user/dto/profile.dto.ts index 2dac53f..3c172f5 100644 --- a/src/user/dto/profile.dto.ts +++ b/src/user/dto/profile.dto.ts @@ -1,5 +1,6 @@ import { OmitType, ApiProperty } from '@nestjs/swagger'; import { UserDTO } from './user.dto'; +import { Expose } from 'class-transformer'; export class ProfileDTO extends OmitType(UserDTO, [ 'password', @@ -8,9 +9,11 @@ export class ProfileDTO extends OmitType(UserDTO, [ @ApiProperty({ description: 'birthDate must be a valid date in YYYY-MM-DD format', }) + @Expose() birthDate: Date; @ApiProperty({ description: 'URL of the profile picture', nullable: true }) + @Expose() profilePicture?: string; @ApiProperty({ description: 'rol of the user' }) diff --git a/src/user/dto/user-create.dto.ts b/src/user/dto/user-create.dto.ts new file mode 100644 index 0000000..a8d6859 --- /dev/null +++ b/src/user/dto/user-create.dto.ts @@ -0,0 +1,5 @@ +import { IntersectionType } from '@nestjs/mapped-types'; +import { UserDTO } from './user.dto'; +import { PasswordDTO } from 'src/auth/dto/password.dto'; + +export class UserCreateDTO extends IntersectionType(UserDTO, PasswordDTO) {} diff --git a/src/user/dto/user-list.dto.ts b/src/user/dto/user-list.dto.ts new file mode 100644 index 0000000..d166ae8 --- /dev/null +++ b/src/user/dto/user-list.dto.ts @@ -0,0 +1,47 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; +import { UserDTO } from './user.dto'; +import { Type, Expose } from 'class-transformer'; +import { ProfileDTO } from './profile.dto'; + +export class UserListDTO extends UserDTO { + @ApiProperty({ description: 'User creation date' }) + @IsNotEmpty() + @Expose() + createdAt: string; + + @ApiProperty({ description: 'User update date' }) + @IsNotEmpty() + @Expose() + updatedAt: string; + + @ApiProperty({ + description: 'The date when the user was deleted, if applicable', + nullable: true, + }) + @Expose() + deletedAt: string | null; + + @ApiProperty({ + description: 'Date the user made their last order (if applicable)', + nullable: true, + }) + @Expose() + lastOrderDate: string | null; + + @ApiProperty({ description: 'User role' }) + @Expose() + @IsNotEmpty() + @Expose() + role: string; + + @ApiProperty({ description: 'User validation status' }) + @IsNotEmpty() + @Expose() + isValidated: boolean; + + @ApiProperty({ description: 'Profile object' }) + @Expose() + @Type(() => ProfileDTO) + profile: ProfileDTO; +} diff --git a/src/user/dto/user.dto.ts b/src/user/dto/user.dto.ts index 6571eb7..773c828 100644 --- a/src/user/dto/user.dto.ts +++ b/src/user/dto/user.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; +import { Transform, Expose } from 'class-transformer'; import { IsDateString, IsEmail, @@ -14,16 +14,19 @@ import { IsOlderThan } from 'src/utils/is-older-than-validator'; export class UserDTO { @ApiProperty({ description: 'The name of the user' }) @IsNotEmpty() + @Expose() firstName: string; @ApiProperty({ description: 'The last name of the user' }) @IsNotEmpty() + @Expose() lastName: string; @ApiProperty({ description: 'The email of the user', uniqueItems: true }) - @Transform(({ value }: { value: string }) => value.trim()) + @Transform(({ value }: { value: string }) => value?.trim()) @IsNotEmpty() @IsEmail() + @Expose() email: string; @ApiProperty({ description: 'the password of the user' }) @@ -34,10 +37,12 @@ export class UserDTO { @ApiProperty({ description: 'the id of the user', uniqueItems: true }) @IsNotEmpty() + @Expose() documentId: string; @ApiProperty({ description: 'the phone number of the user', required: false }) @IsOptional() + @Expose() phoneNumber?: string; @ApiProperty({ description: 'the birth date of the user' }) @@ -58,6 +63,7 @@ export class UserDTO { required: false, enum: UserGender, }) + @Expose() @IsOptional() @IsEnum(UserGender) gender?: UserGender; diff --git a/src/user/entities/user.entity.ts b/src/user/entities/user.entity.ts index e8ea799..eed12be 100644 --- a/src/user/entities/user.entity.ts +++ b/src/user/entities/user.entity.ts @@ -2,6 +2,7 @@ import { BaseModel } from 'src/utils/entity'; import { Entity, Column, OneToOne } from 'typeorm'; import { Exclude } from 'class-transformer'; import type { UserOTP } from './user-otp.entity'; +import { Profile } from './profile.entity'; export enum UserRole { ADMIN = 'admin', @@ -46,4 +47,7 @@ export class User extends BaseModel { @OneToOne('UserOTP', (userOTP: UserOTP) => userOTP.user, { eager: true }) otp: UserOTP; + + @OneToOne(() => Profile, (profile: Profile) => profile.user, { eager: true }) + profile: Profile; } diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 3aa5fee..7ad1c37 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -10,8 +10,17 @@ import { HttpStatus, Get, Param, + DefaultValuePipe, + ParseIntPipe, + Query, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiOkResponse, + getSchemaPath, +} from '@nestjs/swagger'; import { UserService } from './user.service'; import { OtpDTO } from './dto/otp.dto'; import { ProfileDTO } from './dto/profile.dto'; @@ -19,11 +28,21 @@ import { AuthGuard } from 'src/auth/auth.guard'; import { Request } from 'express'; import { User } from './entities/user.entity'; import { UserOrAdminGuard } from 'src/auth/user-or-admin.guard'; +import { RolesGuard } from 'src/auth/roles.guard'; +import { Roles } from 'src/auth/roles.decorador'; +import { Role } from 'src/auth/rol.enum'; +import { UserListDTO } from './dto/user-list.dto'; +import { PaginationDTO } from 'src/utils/dto/pagination.dto'; +import { ConfigService } from '@nestjs/config'; +import { getPaginationUrl } from 'src/utils/pagination-urls'; @ApiTags('User') @Controller('user') export class UserController { - constructor(private readonly userService: UserService) {} + constructor( + private readonly userService: UserService, + private configService: ConfigService, + ) {} @Post('otp') @UseGuards(AuthGuard) @@ -58,4 +77,50 @@ export class UserController { async getProfile(@Param('userId') userId: string): Promise { return this.userService.getUserProfile(userId); } + + @UseGuards(AuthGuard, RolesGuard) + @Roles(Role.ADMIN) + @Get() + @ApiOperation({ + summary: 'Lista de usuarios activos', + description: + 'Devuelve todos los usuarios activos y validados, incluyendo su perfil asociado.', + }) + @ApiOkResponse({ + description: 'Usuarios obtenidos correctamente.', + schema: { + allOf: [ + { $ref: getSchemaPath(PaginationDTO) }, + { + properties: { + results: { + type: 'array', + items: { $ref: getSchemaPath(UserListDTO) }, + }, + }, + }, + ], + }, + }) + async getActiveUsers( + @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, + @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number, + @Req() req: Request, + ): Promise> { + const baseUrl = this.configService.get('API_URL') + `${req.path}`; + const totalItems = await this.userService.countActiveUsers(); + const { next, previous } = getPaginationUrl( + baseUrl, + page, + limit, + totalItems, + ); + const users = await this.userService.getActiveUsers(page, limit); + return { + results: users, + count: totalItems, + next, + previous, + }; + } } diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 1a7e674..d2323dc 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -10,6 +10,8 @@ import { User } from './entities/user.entity'; import { UserOTP } from './entities/user-otp.entity'; import { Profile } from './entities/profile.entity'; import { ProfileDTO } from './dto/profile.dto'; +import { plainToInstance } from 'class-transformer'; +import { UserListDTO } from './dto/user-list.dto'; @Injectable() export class UserService { @@ -134,4 +136,26 @@ export class UserService { role: profile.user.role, }; } + + async countActiveUsers(): Promise { + return await this.userRepository + .createQueryBuilder('user') + .where('user.deletedAt IS NULL') + .getCount(); + } + + async getActiveUsers(page: number, limit: number): Promise { + const users = await this.userRepository + .createQueryBuilder('user') + .leftJoinAndSelect('user.profile', 'profile') + .where('user.deletedAt IS NULL') + .orderBy('user.createdAt', 'DESC') + .skip((page - 1) * limit) + .take(limit) + .getMany(); + + return plainToInstance(UserListDTO, users, { + excludeExtraneousValues: true, + }); + } } From 876574489a1b89bf86d31c4fd29dec215abd9b47 Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Sat, 22 Mar 2025 01:54:24 -0400 Subject: [PATCH 2/4] changed from Spanish to English --- src/user/user.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 7ad1c37..b7b5c51 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -82,12 +82,12 @@ export class UserController { @Roles(Role.ADMIN) @Get() @ApiOperation({ - summary: 'Lista de usuarios activos', + summary: 'List of active users', description: - 'Devuelve todos los usuarios activos y validados, incluyendo su perfil asociado.', + 'Returns all active and validated users, including their associated profile.', }) @ApiOkResponse({ - description: 'Usuarios obtenidos correctamente.', + description: 'Successfully obtained users.', schema: { allOf: [ { $ref: getSchemaPath(PaginationDTO) }, From ade67815c00a2f0a1feda1747cd1060845be7418 Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Sat, 22 Mar 2025 12:03:00 -0400 Subject: [PATCH 3/4] New standards --- src/user/user.controller.ts | 15 ++++++++++----- src/user/user.service.ts | 29 +++++++++++------------------ 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index b7b5c51..9ced562 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -35,6 +35,7 @@ import { UserListDTO } from './dto/user-list.dto'; import { PaginationDTO } from 'src/utils/dto/pagination.dto'; import { ConfigService } from '@nestjs/config'; import { getPaginationUrl } from 'src/utils/pagination-urls'; +import { plainToInstance } from 'class-transformer'; @ApiTags('User') @Controller('user') @@ -82,12 +83,12 @@ export class UserController { @Roles(Role.ADMIN) @Get() @ApiOperation({ - summary: 'List of active users', + summary: 'Lista de usuarios activos', description: - 'Returns all active and validated users, including their associated profile.', + 'Retorna todos los usuarios activos y validados, incluyendo su perfil asociado.', }) @ApiOkResponse({ - description: 'Successfully obtained users.', + description: 'Usuarios obtenidos exitosamente.', schema: { allOf: [ { $ref: getSchemaPath(PaginationDTO) }, @@ -107,7 +108,7 @@ export class UserController { @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number, @Req() req: Request, ): Promise> { - const baseUrl = this.configService.get('API_URL') + `${req.path}`; + const baseUrl = this.configService.get('API_URL') + req.path; const totalItems = await this.userService.countActiveUsers(); const { next, previous } = getPaginationUrl( baseUrl, @@ -116,8 +117,12 @@ export class UserController { totalItems, ); const users = await this.userService.getActiveUsers(page, limit); + const usersDTO = plainToInstance(UserListDTO, users, { + excludeExtraneousValues: true, + }); + return { - results: users, + results: usersDTO, count: totalItems, next, previous, diff --git a/src/user/user.service.ts b/src/user/user.service.ts index d2323dc..9000551 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -10,8 +10,7 @@ import { User } from './entities/user.entity'; import { UserOTP } from './entities/user-otp.entity'; import { Profile } from './entities/profile.entity'; import { ProfileDTO } from './dto/profile.dto'; -import { plainToInstance } from 'class-transformer'; -import { UserListDTO } from './dto/user-list.dto'; +import { IsNull } from 'typeorm'; @Injectable() export class UserService { @@ -138,24 +137,18 @@ export class UserService { } async countActiveUsers(): Promise { - return await this.userRepository - .createQueryBuilder('user') - .where('user.deletedAt IS NULL') - .getCount(); + return this.userRepository.count({ + where: { deletedAt: IsNull() }, + }); } - async getActiveUsers(page: number, limit: number): Promise { - const users = await this.userRepository - .createQueryBuilder('user') - .leftJoinAndSelect('user.profile', 'profile') - .where('user.deletedAt IS NULL') - .orderBy('user.createdAt', 'DESC') - .skip((page - 1) * limit) - .take(limit) - .getMany(); - - return plainToInstance(UserListDTO, users, { - excludeExtraneousValues: true, + async getActiveUsers(page: number, limit: number): Promise { + return this.userRepository.find({ + where: { deletedAt: IsNull() }, + relations: ['profile'], + order: { createdAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, }); } } From df16bcfd8b42a6632e0f5edb00f9ffa0c1c71676 Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Sat, 22 Mar 2025 12:08:08 -0400 Subject: [PATCH 4/4] change from Spanish to English, controller --- src/user/user.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 9ced562..25e8389 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -83,12 +83,12 @@ export class UserController { @Roles(Role.ADMIN) @Get() @ApiOperation({ - summary: 'Lista de usuarios activos', + summary: 'List of active users', description: - 'Retorna todos los usuarios activos y validados, incluyendo su perfil asociado.', + 'Returns all active and validated users, including their associated profile.', }) @ApiOkResponse({ - description: 'Usuarios obtenidos exitosamente.', + description: 'Users successfully obtained', schema: { allOf: [ { $ref: getSchemaPath(PaginationDTO) },