user
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { IsOptional, IsString, IsNumber, Min, IsEnum, IsIn } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { EducationLevel, User } from '../entities/user.entity';
|
||||
|
||||
// Define the valid sort directions
|
||||
const sortOrderOptions = ['asc', 'desc'] as const;
|
||||
type SortOrder = (typeof sortOrderOptions)[number];
|
||||
|
||||
export class FindUsersDto {
|
||||
/**
|
||||
* The page number to retrieve.
|
||||
* @default 1
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'The page number to retrieve.',
|
||||
type: Number,
|
||||
default: 1,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
page?: number = 1;
|
||||
|
||||
/**
|
||||
* The number of items per page.
|
||||
* @default 10
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'The number of items per page.',
|
||||
type: Number,
|
||||
default: 10,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
limit?: number = 10;
|
||||
|
||||
/**
|
||||
* A general search term to filter by.
|
||||
* Searches firstName, lastName, phone, and nationalCode.
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'A general search term (firstName, lastName, phone, nationalCode).',
|
||||
type: String,
|
||||
example: 'John Doe',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
|
||||
/**
|
||||
* Filter by a specific education level.
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Filter by a specific education level.',
|
||||
enum: EducationLevel, // Use the actual enum
|
||||
example: EducationLevel.bachelor,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(EducationLevel)
|
||||
education?: EducationLevel;
|
||||
|
||||
/**
|
||||
* The field to sort the results by.
|
||||
* @default "createdAt"
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'The field to sort the results by.',
|
||||
type: String,
|
||||
default: 'createdAt',
|
||||
// You can provide a list of valid keys for better documentation
|
||||
// You must ensure 'keyof User' is correctly exported/available here
|
||||
example: 'lastName',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
orderBy?: keyof User = 'createdAt';
|
||||
|
||||
/**
|
||||
* The direction to sort the results.
|
||||
* @default "desc"
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'The direction to sort the results.',
|
||||
enum: sortOrderOptions, // Use the constant array
|
||||
default: 'desc',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(sortOrderOptions)
|
||||
order?: SortOrder = 'desc';
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { IsString, IsNotEmpty, IsBoolean, IsNumber, IsEnum, MinLength } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { EducationLevel } from '../entities/user.entity';
|
||||
|
||||
export class UpdateUserDto {
|
||||
@ApiProperty({ example: ' ', description: "User's first name" })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
firstName: string;
|
||||
|
||||
@ApiProperty({ example: ' ', description: "User's last name" })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
lastName: string;
|
||||
|
||||
@ApiProperty({ example: ' ', description: "User's father's name" })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
fatherName: string;
|
||||
|
||||
// --- Personal Details ---
|
||||
|
||||
@ApiPropertyOptional({ example: true, description: 'Gender: true for male, false for female' })
|
||||
@IsNotEmpty()
|
||||
@IsBoolean()
|
||||
isMale: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: '1234567890', description: 'Unique national code' })
|
||||
@IsNotEmpty()
|
||||
@MinLength(10)
|
||||
nationalCode: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '1001', description: 'Employee personal code' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
personalCode: string;
|
||||
|
||||
// --- Employment Details ---
|
||||
|
||||
@ApiPropertyOptional({ example: 'قراردادی', description: 'Type of hire (e.g., permanent, temporary)' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
hireType: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'اراک', description: 'Primary work location' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
workLocation: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 5, description: 'Years of work experience' })
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
workExperienceYear: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: EducationLevel,
|
||||
example: EducationLevel.master,
|
||||
description: 'Highest level of education achieved',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsEnum(EducationLevel)
|
||||
education: EducationLevel;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Entity, ManyToOne, Opt, Property } from "@mikro-orm/core";
|
||||
|
||||
import { User } from "./user.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
|
||||
@Entity()
|
||||
export class RefreshToken extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 255 })
|
||||
token!: string;
|
||||
|
||||
@ManyToOne(() => User, { deleteRule: "cascade" })
|
||||
user!: User;
|
||||
|
||||
@Property({ type: "timestamptz" })
|
||||
expiresAt!: Date;
|
||||
|
||||
@Property({ default: false })
|
||||
isRevoked: boolean & Opt;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Entity, Enum, Property, Filter, BaseEntity } from '@mikro-orm/core';
|
||||
|
||||
export enum EducationLevel {
|
||||
diploma = 'دیپلم',
|
||||
associate = 'کاردانی',
|
||||
bachelor = 'کارشناسی',
|
||||
master = 'کارشناسی ارشد',
|
||||
phd = 'دکتری',
|
||||
}
|
||||
|
||||
export enum UserStatus {
|
||||
finished = 'finished',
|
||||
pending = 'pending',
|
||||
}
|
||||
|
||||
@Filter({ name: 'notDeleted', cond: { deletedAt: null }, default: true })
|
||||
@Entity({ tableName: 'users' })
|
||||
export class User extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
firstName?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
lastName?: string;
|
||||
|
||||
@Property({ unique: true })
|
||||
phone!: string;
|
||||
|
||||
@Enum({
|
||||
items: () => UserStatus,
|
||||
nullable: true,
|
||||
default: UserStatus.pending,
|
||||
})
|
||||
status?: UserStatus;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Controller, Get, Patch, Body, Req, UseGuards, Query, ValidationPipe } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody } from '@nestjs/swagger';
|
||||
import { AuthGuard, type AuthRequest } from 'src/modules/auth/guards/auth.guard';
|
||||
import { UserService } from './user.service';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { FindUsersDto } from './dto/find-user.dto';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/auth.admin.guard';
|
||||
|
||||
@ApiTags('User')
|
||||
@Controller('user')
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get the current authenticated user profile' })
|
||||
@Get('/')
|
||||
async getUser(@Req() req: AuthRequest) {
|
||||
const userId = req.user.sub;
|
||||
const user = await this.userService.findById(+userId);
|
||||
return {
|
||||
message: `GET request: Retrieving profile for authenticated user `,
|
||||
user,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Update User Specification (PATCH /user/spec)
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Update the user specification' })
|
||||
@ApiBody({ type: UpdateUserDto })
|
||||
@Patch('/')
|
||||
async updateUserSpec(@Req() req: AuthRequest, @Body() dto: UpdateUserDto) {
|
||||
const userId = req.user.sub;
|
||||
const user = await this.userService.updateUser(+userId, dto);
|
||||
return {
|
||||
message: `PATCH request: Updating specification for user ${userId} `,
|
||||
userId,
|
||||
user,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Users : Admin Route' })
|
||||
@Get('/admin/users')
|
||||
async findAll(
|
||||
// Use ValidationPipe to validate and transform the DTO
|
||||
@Query(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
query: FindUsersDto,
|
||||
) {
|
||||
return this.userService.findAll(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { UserController } from './user.controller';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { User } from './entities/user.entity';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
@Module({
|
||||
providers: [UserService],
|
||||
controllers: [UserController],
|
||||
imports: [MikroOrmModule.forFeature([User]), JwtModule],
|
||||
exports: [UserService],
|
||||
})
|
||||
export class UserModule {}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
|
||||
import { User, UserStatus } from './entities/user.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { FindUsersDto } from './dto/find-user.dto';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: EntityRepository<User>,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
async findOrCreateByPhone(phone: string): Promise<User> {
|
||||
let user = await this.userRepository.findOne({ phone });
|
||||
|
||||
if (!user) {
|
||||
user = this.userRepository.create({
|
||||
phone,
|
||||
});
|
||||
await this.em.persistAndFlush(user);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async updateUser(userId: number, dto: UpdateUserDto): Promise<User> {
|
||||
const user = await this.em.findOneOrFail(User, userId, {}).catch(() => {
|
||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||
});
|
||||
|
||||
this.em.assign(user, { ...dto, status: UserStatus.finished });
|
||||
|
||||
await this.em.flush();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async findByPhone(phone: string): Promise<User | null> {
|
||||
return this.userRepository.findOne({ phone });
|
||||
}
|
||||
|
||||
async findById(id: number): Promise<User | null> {
|
||||
return this.userRepository.findOne({ id });
|
||||
}
|
||||
|
||||
async create(phone: string): Promise<User> {
|
||||
const user = this.userRepository.create({ phone });
|
||||
await this.em.persistAndFlush(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
async findAll(dto: FindUsersDto): Promise<PaginatedResult<User>> {
|
||||
const { page = 1, limit = 10, search, education, orderBy = 'createdAt', order = 'desc' } = dto;
|
||||
|
||||
// 1. Calculate pagination
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// 2. Build the 'where' filter query
|
||||
const where: FilterQuery<User> = {};
|
||||
|
||||
if (education) {
|
||||
where.education = education;
|
||||
}
|
||||
|
||||
// 4. Add 'search' logic (case-insensitive)
|
||||
if (search) {
|
||||
const searchPattern = `%${search}%`;
|
||||
// $ilike is case-insensitive (PostgreSQL). Use $like for MySQL (often CI by default)
|
||||
where.$or = [
|
||||
{ firstName: { $ilike: searchPattern } },
|
||||
{ lastName: { $ilike: searchPattern } },
|
||||
{ phone: { $ilike: searchPattern } },
|
||||
{ nationalCode: { $ilike: searchPattern } },
|
||||
{ personalCode: { $ilike: searchPattern } },
|
||||
];
|
||||
}
|
||||
|
||||
// 5. Execute the query using findAndCount
|
||||
const [users, total] = await this.userRepository.findAndCount(where, {
|
||||
limit,
|
||||
offset,
|
||||
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
||||
});
|
||||
|
||||
// 6. Calculate total pages
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
// 7. Return the paginated result
|
||||
return {
|
||||
data: users,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user