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 11aecd4..140c76d 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().toLowerCase()) @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 fc45a0b..d47c0f6 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -10,9 +10,18 @@ import { HttpStatus, Get, Param, + DefaultValuePipe, + ParseIntPipe, + Query, Delete, } 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'; @@ -20,11 +29,22 @@ 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'; +import { plainToInstance } from 'class-transformer'; @ApiTags('User') @Controller('user') export class UserController { - constructor(private readonly userService: UserService) {} + constructor( + private readonly userService: UserService, + private configService: ConfigService, + ) {} @Post('otp') @UseGuards(AuthGuard) @@ -60,6 +80,56 @@ export class UserController { return this.userService.getUserProfile(userId); } + @UseGuards(AuthGuard, RolesGuard) + @Roles(Role.ADMIN) + @Get() + @ApiOperation({ + summary: 'List of active users', + description: + 'Returns all active and validated users, including their associated profile.', + }) + @ApiOkResponse({ + description: 'Users successfully obtained', + 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); + const usersDTO = plainToInstance(UserListDTO, users, { + excludeExtraneousValues: true, + }); + + return { + results: usersDTO, + count: totalItems, + next, + previous, + }; + } + @Delete(':userId') @UseGuards(AuthGuard, UserOrAdminGuard) @ApiOperation({ summary: 'Delete user logically' }) diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 5b98b23..2c56e3c 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -11,6 +11,7 @@ import { UserOTP } from './entities/user-otp.entity'; import { Profile } from './entities/profile.entity'; import { OTPType } from 'src/user/entities/user-otp.entity'; import { ProfileDTO } from './dto/profile.dto'; +import { IsNull } from 'typeorm'; @Injectable() export class UserService { @@ -137,6 +138,22 @@ export class UserService { }; } + async countActiveUsers(): Promise { + return this.userRepository.count({ + where: { deletedAt: IsNull() }, + }); + } + + 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, + }); + } + async deleteUser(userId: string): Promise { const userToDelete = await this.userRepository.findOneBy({ id: userId }); if (!userToDelete) {