Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/user/dto/profile.dto.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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' })
Expand Down
5 changes: 5 additions & 0 deletions src/user/dto/user-create.dto.ts
Original file line number Diff line number Diff line change
@@ -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) {}
47 changes: 47 additions & 0 deletions src/user/dto/user-list.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
8 changes: 7 additions & 1 deletion src/user/dto/user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { Transform, Expose } from 'class-transformer';
import {
IsDateString,
IsEmail,
Expand All @@ -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' })
Expand All @@ -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' })
Expand All @@ -58,6 +63,7 @@ export class UserDTO {
required: false,
enum: UserGender,
})
@Expose()
@IsOptional()
@IsEnum(UserGender)
gender?: UserGender;
Expand Down
4 changes: 4 additions & 0 deletions src/user/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;
}
74 changes: 72 additions & 2 deletions src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,41 @@ 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';
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)
Expand Down Expand Up @@ -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<PaginationDTO<UserListDTO>> {
const baseUrl = this.configService.get<string>('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' })
Expand Down
17 changes: 17 additions & 0 deletions src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -137,6 +138,22 @@ export class UserService {
};
}

async countActiveUsers(): Promise<number> {
return this.userRepository.count({
where: { deletedAt: IsNull() },
});
}

async getActiveUsers(page: number, limit: number): Promise<User[]> {
return this.userRepository.find({
where: { deletedAt: IsNull() },
relations: ['profile'],
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});
}

async deleteUser(userId: string): Promise<void> {
const userToDelete = await this.userRepository.findOneBy({ id: userId });
if (!userToDelete) {
Expand Down