This commit is contained in:
2025-11-10 22:42:32 +03:30
parent 45ca2fde06
commit bca4b75b6a
21 changed files with 54 additions and 654 deletions
-2
View File
@@ -14,7 +14,6 @@ import { UploaderModule } from './modules/uploader/uploader.module';
import { AdminModule } from './modules/admin/admin.module'; import { AdminModule } from './modules/admin/admin.module';
// import { CacheService } from './modules/utils/cache.service'; // import { CacheService } from './modules/utils/cache.service';
import { ThrottlerModule } from '@nestjs/throttler'; import { ThrottlerModule } from '@nestjs/throttler';
import { SubmitionModule } from './modules/submition/submition.module';
@Module({ @Module({
imports: [ imports: [
@@ -42,7 +41,6 @@ import { SubmitionModule } from './modules/submition/submition.module';
limit: 5, // max requests per window limit: 5, // max requests per window
}, },
]), ]),
SubmitionModule,
], ],
controllers: [], controllers: [],
// providers: [CacheService], // providers: [CacheService],
+3 -1
View File
@@ -1,5 +1,7 @@
import { PrimaryKey, Property, sql, OptionalProps } from '@mikro-orm/core'; import { PrimaryKey, Property, sql, Filter, OptionalProps } from '@mikro-orm/core';
import { ulid } from 'ulid';
@Filter({ name: 'notDeleted', cond: { deletedAt: null }, default: true })
export abstract class BaseEntity { export abstract class BaseEntity {
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt' | 'deletedAt'; [OptionalProps]?: 'id' | 'createdAt' | 'updatedAt' | 'deletedAt';
+1 -1
View File
@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
import { MikroOrmModule } from '@mikro-orm/nestjs'; import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Admin } from './entities/user.entity'; import { Admin } from './entities/admin.entity';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
@Module({ @Module({
+7 -7
View File
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@mikro-orm/nestjs'; import { InjectRepository } from '@mikro-orm/nestjs';
import { EntityRepository } from '@mikro-orm/core'; import { EntityRepository } from '@mikro-orm/core';
import { Admin } from './entities/user.entity'; import { Admin } from './entities/admin.entity';
import { EntityManager } from '@mikro-orm/postgresql'; import { EntityManager } from '@mikro-orm/postgresql';
@Injectable() @Injectable()
@@ -16,13 +16,13 @@ export class AdminService {
return this.adminRepository.findOne({ phone }); return this.adminRepository.findOne({ phone });
} }
async findById(id: number): Promise<Admin | null> { async findById(id: string): Promise<Admin | null> {
return this.adminRepository.findOne({ id }); return this.adminRepository.findOne({ id });
} }
async create(phone: string): Promise<Admin> { // async create(phone: string): Promise<Admin> {
const user = this.adminRepository.create({ phone }); // const user = this.adminRepository.create({ phone });
await this.em.persistAndFlush(user); // await this.em.persistAndFlush(user);
return user; // return user;
} // }
} }
@@ -0,0 +1,14 @@
import { Entity, Property } from '@mikro-orm/core';
import { BaseEntity } from 'src/common/entities/base.entity';
@Entity({ tableName: 'admins' })
export class Admin extends BaseEntity {
@Property({ nullable: true })
firstName?: string;
@Property({ nullable: true })
lastName?: string;
@Property({ unique: true })
phone!: string;
}
-14
View File
@@ -1,14 +0,0 @@
import { Collection, Entity, Enum, OneToMany } from '@mikro-orm/core';
import { User } from '../../users/entities/user.entity';
import { BaseEntity } from '../../../common/entities/base.entity';
import { RoleEnum } from '../enums/role.enum';
@Entity()
export class Role extends BaseEntity {
@Enum({ items: () => RoleEnum, nativeEnumName: 'role_enum', default: RoleEnum.USER })
name!: RoleEnum;
@OneToMany(() => User, user => user.role)
users = new Collection<User>(this);
}
-24
View File
@@ -1,24 +0,0 @@
import { Entity, PrimaryKey, Property, OptionalProps } from '@mikro-orm/core';
@Entity({ tableName: 'admns' })
export class Admin {
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt';
@PrimaryKey()
id!: number;
@Property({ nullable: true })
firstName?: string;
@Property({ nullable: true })
lastName?: string;
@Property({ unique: true })
phone!: string;
@Property({ defaultRaw: 'now()', columnType: 'timestamptz' })
createdAt: Date = new Date();
@Property({ onUpdate: () => new Date(), defaultRaw: 'now()', columnType: 'timestamptz' })
updatedAt: Date = new Date();
}
@@ -1,9 +1,5 @@
import { RoleEnum } from "../../users/enums/role.enum";
export interface ILocalTokenPayload { export interface ILocalTokenPayload {
id: string; id: string;
role: RoleEnum;
wildduckUserId: string;
} }
export interface IConsoleTokenPayload { export interface IConsoleTokenPayload {
+1 -1
View File
@@ -55,7 +55,7 @@ export class AuthService {
const user = await this.userService.findOrCreateByPhone(phone); const user = await this.userService.findOrCreateByPhone(phone);
const accessToken = await this.issueToken(user.id.toString(), AuthEntityType.USER); const accessToken = await this.issueToken(user.id, AuthEntityType.USER);
return { success: true, message: 'User registered successfully!', accessToken }; return { success: true, message: 'User registered successfully!', accessToken };
} }
+4 -5
View File
@@ -24,7 +24,6 @@ export class TokensService {
return this.generateAccessAndRefreshToken( return this.generateAccessAndRefreshToken(
{ {
id: user.id, id: user.id,
role: user.role.name,
}, },
em, em,
); );
@@ -96,21 +95,21 @@ export class TokensService {
RefreshToken, RefreshToken,
{ token: oldRefreshToken }, { token: oldRefreshToken },
{ {
populate: ['user', 'user.role'], populate: ['user'],
}, },
); );
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN); if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
if (dayjs(token.expiresAt).isBefore(dayjs())) { if (dayjs(token.expiresAt).isBefore(dayjs())) {
await entityManager.remove(token); entityManager.remove(token);
await entityManager.flush(); await entityManager.flush();
await entityManager.commit(); await entityManager.commit();
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED); throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
} }
// Remove the old token // Remove the old token
await entityManager.remove(token); entityManager.remove(token);
// Generate new tokens within the same transaction // Generate new tokens within the same transaction
const tokens = await this.generateTokens(token.user, entityManager); const tokens = await this.generateTokens(token.user, entityManager);
@@ -148,7 +147,7 @@ export class TokensService {
} }
} }
private async generateRefreshToken() { private generateRefreshToken() {
return randomBytes(32).toString('hex'); return randomBytes(32).toString('hex');
} }
} }
@@ -1,29 +0,0 @@
import { IsString, IsNotEmpty, IsDate, IsOptional } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class CreateSubmissioncDto {
@ApiPropertyOptional({ example: 'صداو سیما', description: 'دستگاه همکار : مثلا صدا و سیما' })
@IsOptional()
@IsString()
cooperator: string;
@ApiPropertyOptional({ example: 'ملی', description: 'ملی|استانی|' })
@IsOptional()
@IsString()
cooperationLevel: string;
@ApiProperty({
example: '2025-11-03T12:30:00.000Z',
description: 'The date and time of the submission (ISO 8601 format).',
})
@IsNotEmpty({ message: 'Submission Date is required.' })
@IsDate({ message: 'Submission Date must be a valid ISO 8601 date.' })
@Type(() => Date)
submissionDate: Date;
@ApiProperty({ example: 'مقاله', description: ' ' })
@IsNotEmpty()
@IsString()
submissionType: string;
}
@@ -1,109 +0,0 @@
import { IsOptional, IsString, IsNumber, Min, IsEnum, IsIn } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Submition, SubmitionStatus } from '../entities/submition.entity';
const sortOrderOptions = ['asc', 'desc'] as const;
type SortOrder = (typeof sortOrderOptions)[number];
export class FindSubmitionsDto {
/**
* 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 user status.
*/
@ApiPropertyOptional({
description: 'Filter by a specific user status.',
enum: SubmitionStatus, // Use the actual enum
example: SubmitionStatus.finished,
})
@IsOptional()
@IsEnum(SubmitionStatus)
status?: SubmitionStatus;
@ApiPropertyOptional({
description: 'Filter by a cooperationLevel.',
example: 'ملی',
})
@IsOptional()
@IsString()
cooperationLevel?: string;
@ApiPropertyOptional({
description: 'Filter by a submissionType.',
example: 'مقاله',
})
@IsOptional()
@IsString()
submissionType?: string;
/**
* The field to sort the results by.
* @default "user"
*/
@ApiPropertyOptional({
description: 'The field to sort the results by.',
type: String,
default: 'user',
example: 'user',
})
@IsOptional()
@IsString()
orderBy?: keyof Submition = 'user';
/**
* 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';
}
@@ -1,45 +0,0 @@
import {
IsString,
IsNotEmpty,
IsArray,
ArrayMinSize,
Validate,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
@ValidatorConstraint({ name: 'MaxWordCount', async: false })
export class MaxWordCount implements ValidatorConstraintInterface {
validate(text: string) {
if (typeof text !== 'string') return false;
const wordCount = text.trim().split(/\s+/).filter(Boolean).length;
return wordCount <= 10000;
}
defaultMessage() {
return 'Submission description must not exceed 1000 words.';
}
}
export class UpdateDocsDto {
@ApiProperty({
example: 'توضیحاتی در مورد مدارک ارسالی و هدف از ارسال.',
description: 'Detailed description or notes regarding the documents being submitted (maximum 1000 words).',
})
@IsNotEmpty({ message: 'Submission description is required.' })
@IsString({ message: 'Submission description must be a string.' })
@Validate(MaxWordCount)
submissionDesc: string;
@ApiProperty({
example: ['https://example.com/doc1.pdf', 'https://example.com/doc2.jpg'],
description: 'An array of URLs pointing to the submitted documents.',
type: [String],
})
@IsNotEmpty({ message: 'Document URLs are required.' })
@IsArray({ message: 'Document URLs must be an array.' })
@ArrayMinSize(1, { message: 'At least one document URL is required.' })
@IsString({ each: true, message: 'Each document URL must be a valid string.' })
docsUrl: string[];
}
@@ -1,29 +0,0 @@
import { IsString, IsNotEmpty, IsDate, IsOptional } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class UpdateSubmissioncDto {
@ApiPropertyOptional({ example: 'صداو سیما', description: 'دستگاه همکار : مثلا صدا و سیما' })
@IsOptional()
@IsString()
cooperator: string;
@ApiPropertyOptional({ example: 'ملی', description: 'ملی|استانی|' })
@IsOptional()
@IsString()
cooperationLevel: string;
@ApiProperty({
example: '2025-11-03T12:30:00.000Z',
description: 'The date and time of the submission (ISO 8601 format).',
})
@IsNotEmpty({ message: 'Submission Date is required.' })
@IsDate({ message: 'Submission Date must be a valid ISO 8601 date.' })
@Type(() => Date)
submissionDate: Date;
@ApiProperty({ example: 'مقاله', description: ' ' })
@IsNotEmpty()
@IsString()
submissionType: string;
}
@@ -1,48 +0,0 @@
import { Entity, Enum, PrimaryKey, Property, OptionalProps, ManyToOne } from '@mikro-orm/core';
import { User } from '../../users/entities/user.entity';
export enum SubmitionStatus {
finished = 'تکمیل',
pending = 'ناقص',
}
@Entity({ tableName: 'submition' })
export class Submition {
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt';
@ManyToOne(() => User)
user!: User;
@PrimaryKey()
id!: number;
@Enum({ items: () => SubmitionStatus, nullable: true, default: SubmitionStatus.pending })
status?: SubmitionStatus;
@Property({ nullable: true })
submissionType?: string;
@Property({ nullable: true })
cooperator?: string;
@Property({ nullable: true })
cooperationLevel?: string;
@Property({ nullable: true, columnType: 'text' })
submissionDesc?: string;
@Property({ type: 'text[]', nullable: true })
docsUrl?: string[];
@Property({ columnType: 'timestamptz', nullable: true })
submissionDate?: Date;
@Property({ defaultRaw: 'now()', columnType: 'timestamptz' })
createdAt: Date = new Date();
@Property({ onUpdate: () => new Date(), defaultRaw: 'now()', columnType: 'timestamptz' })
updatedAt: Date = new Date();
@Property({ nullable: true, columnType: 'timestamptz' })
deletedAt?: Date;
}
@@ -1,135 +0,0 @@
import {
Controller,
Get,
Patch,
Body,
Req,
UseGuards,
Query,
ValidationPipe,
Post,
Param,
ParseIntPipe,
Delete,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody, ApiParam } from '@nestjs/swagger';
import { AuthGuard, type AuthRequest } from 'src/modules/auth/guards/auth.guard';
import { SubmitionService } from './submition.service';
import { UpdateSubmissioncDto } from './dto/update-submission.dto';
import { UpdateDocsDto } from './dto/update-docs.dto';
import { AdminAuthGuard } from 'src/modules/auth/guards/auth.admin.guard';
import { FindSubmitionsDto } from './dto/find-submitions.dto';
import { CreateSubmissioncDto } from './dto/create-submission.dto';
import { SubmitionStatus } from './entities/submition.entity';
@ApiTags('submition')
@Controller('submition')
export class SubmitionController {
constructor(private readonly submitionService: SubmitionService) {}
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get User submissions' })
@Get('/')
async getUserSbmitions(@Req() req: AuthRequest) {
const userId = req.user.sub;
const submitions = await this.submitionService.findByUserId(+userId);
return {
message: `User submissions `,
submitions,
};
}
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get single submission' })
@ApiParam({ name: 'id', description: 'The ID of the submission ', type: Number })
@Get(':id')
async getSbmition(@Req() req: AuthRequest, @Param('id', ParseIntPipe) id: number) {
const userId = req.user.sub;
const submition = await this.submitionService.findById(id, +userId);
return {
message: `Success `,
submition,
};
}
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Create submission :first step of creation' })
@ApiBody({ type: CreateSubmissioncDto })
@Post('/')
async createSubmition(@Req() req: AuthRequest, @Body() dto: CreateSubmissioncDto) {
const userId = req.user.sub;
const submition = await this.submitionService.create(+userId, dto);
return {
message: `Created submission `,
userId,
submition,
};
}
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiParam({ name: 'id', description: 'The ID of the submission to update', type: Number })
@ApiOperation({ summary: 'Update the submission : first step of update' })
@ApiBody({ type: UpdateSubmissioncDto })
@Patch(':id')
async updateUserSubmissionType(
@Req() req: AuthRequest,
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateSubmissioncDto,
) {
const userId = req.user.sub;
const submition = await this.submitionService.updateSubmition(id, +userId, dto);
return {
message: `PATCH request: Updating submission type for user done`,
userId,
submition,
};
}
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiParam({ name: 'id', description: 'The ID of the submission to update', type: Number })
@ApiOperation({ summary: 'Update the user document/file reference' })
@ApiBody({ type: UpdateDocsDto })
@Patch(':id/doc')
async updateUserDoc(@Req() req: AuthRequest, @Param('id', ParseIntPipe) id: number, @Body() dto: UpdateDocsDto) {
const userId = req.user.sub;
const submition = await this.submitionService.updateSubmition(id, +userId, {
...dto,
status: SubmitionStatus.finished,
});
return {
message: `PATCH request: Updating doc field for user ${userId} `,
userId,
submition,
};
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'submissions' })
@Get('/admin/submissions')
async findAll(
@Query(new ValidationPipe({ transform: true, whitelist: true }))
query: FindSubmitionsDto,
) {
return this.submitionService.findAll(query);
}
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Delete submission' })
@ApiParam({ name: 'id', description: 'The ID of the submission ', type: Number })
@Delete(':id')
async deleteSbmition(@Req() req: AuthRequest, @Param('id', ParseIntPipe) id: number) {
const userId = req.user.sub;
const submition = await this.submitionService.deleteSubmition(id, +userId);
return {
message: `submition deleted successfully `,
submition,
};
}
}
-15
View File
@@ -1,15 +0,0 @@
import { Module } from '@nestjs/common';
import { SubmitionService } from './submition.service';
import { SubmitionController } from './submition.controller';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { JwtModule } from '@nestjs/jwt';
import { Submition } from './entities/submition.entity';
// import { AuthModule } from '../auth/auth.module';
@Module({
providers: [SubmitionService],
controllers: [SubmitionController],
imports: [MikroOrmModule.forFeature([Submition]), JwtModule],
exports: [SubmitionService],
})
export class SubmitionModule {}
-140
View File
@@ -1,140 +0,0 @@
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@mikro-orm/nestjs';
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
import { SubmitionStatus, Submition } from './entities/submition.entity';
import { EntityManager } from '@mikro-orm/postgresql';
import { UpdateSubmissioncDto } from './dto/update-submission.dto';
import { UpdateDocsDto } from './dto/update-docs.dto';
import { FindSubmitionsDto } from './dto/find-submitions.dto';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { CreateSubmissioncDto } from './dto/create-submission.dto';
@Injectable()
export class SubmitionService {
constructor(
@InjectRepository(Submition)
private readonly submitionRepository: EntityRepository<Submition>,
private readonly em: EntityManager,
) {}
async updateSubmition(
submitionId: number,
userId: number,
dto: UpdateSubmissioncDto | (UpdateDocsDto & { status: SubmitionStatus }),
): Promise<Submition> {
const sub = await this.em.findOneOrFail(Submition, submitionId, { populate: ['user'] }).catch(() => {
throw new NotFoundException(`submition with ID ${submitionId} not found.`);
});
if (sub.user.id !== userId) {
throw new ForbiddenException(`User is not the owner of submition ID ${submitionId}.`);
}
this.em.assign(sub, dto);
await this.em.flush();
return sub;
}
async findById(submitionId: number, userId: number): Promise<Submition | null> {
return this.submitionRepository.findOne({ id: submitionId, user: { id: userId }, deletedAt: null });
}
async findByUserId(userId: number): Promise<Submition[] | null> {
return this.submitionRepository.find({ user: userId, deletedAt: null });
}
async create(userId: number, dto: CreateSubmissioncDto): Promise<Submition> {
const submition = this.submitionRepository.create({ ...dto, user: userId });
await this.em.persistAndFlush(submition);
return submition;
}
async findAll(dto: FindSubmitionsDto): Promise<PaginatedResult<Submition>> {
const {
page = 1,
limit = 10,
search,
status,
cooperationLevel,
submissionType,
orderBy = 'id',
order = 'desc',
} = dto;
// 1. Calculate pagination
const offset = (page - 1) * limit;
// 2. Build the 'where' filter query
const where: FilterQuery<Submition> = {
deletedAt: null,
};
// 3. Add exact-match filters (if provided)
if (status) {
where.status = status;
}
if (submissionType) {
where.submissionType = submissionType;
}
if (cooperationLevel) {
where.cooperationLevel = cooperationLevel;
}
// 4. Add 'search' logic (case-insensitive)
if (search) {
const searchPattern = `%${search}%`;
where.$or = [
{ submissionDesc: { $ilike: searchPattern } },
{ cooperator: { $ilike: searchPattern } },
{ user: { firstName: { $ilike: searchPattern } } },
{ user: { lastName: { $ilike: searchPattern } } },
{ user: { nationalCode: { $ilike: searchPattern } } },
{ user: { personalCode: { $ilike: searchPattern } } },
{ user: { phone: { $ilike: searchPattern } } },
];
}
// 5. Execute the query using findAndCount
const [sumitions, total] = await this.submitionRepository.findAndCount(where, {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
populate: ['user'],
});
// 6. Calculate total pages
const totalPages = Math.ceil(total / limit);
// 7. Return the paginated result
return {
data: sumitions,
meta: {
total,
page,
limit,
totalPages,
},
};
}
async deleteSubmition(submitionId: number, userId: number): Promise<Submition> {
const submission = await this.em.findOneOrFail(Submition, submitionId, { populate: ['user'] }).catch(() => {
throw new NotFoundException(`Submition with ID ${submitionId} not found.`);
});
// Ownership Check (same as before)
if (submission.user.id !== userId) {
throw new ForbiddenException(`User with ID ${userId} is not authorized to delete submition ID ${submitionId}.`);
}
// Execute the Soft Delete: Update the timestamp
submission.deletedAt = new Date();
await this.em.flush();
return submission;
}
}
@@ -1,17 +1,16 @@
import { Entity, ManyToOne, Opt, Property } from "@mikro-orm/core"; import { Entity, ManyToOne, Opt, Property } from '@mikro-orm/core';
import { User } from "./user.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from './user.entity';
import { BaseEntity } from 'src/common/entities/base.entity';
@Entity() @Entity()
export class RefreshToken extends BaseEntity { export class RefreshToken extends BaseEntity {
@Property({ type: "varchar", length: 255 }) @Property({ type: 'varchar', length: 255 })
token!: string; token!: string;
@ManyToOne(() => User, { deleteRule: "cascade" }) @ManyToOne(() => User, { deleteRule: 'cascade' })
user!: User; user!: User;
@Property({ type: "timestamptz" }) @Property({ type: 'timestamptz' })
expiresAt!: Date; expiresAt!: Date;
@Property({ default: false }) @Property({ default: false })
+7 -21
View File
@@ -1,19 +1,6 @@
import { Entity, Enum, Property, Filter, BaseEntity } from '@mikro-orm/core'; import { Entity, Property, BaseEntity, OneToMany, Collection } from '@mikro-orm/core';
import { RefreshToken } from './refresh-token.entity';
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' }) @Entity({ tableName: 'users' })
export class User extends BaseEntity { export class User extends BaseEntity {
@Property({ nullable: true }) @Property({ nullable: true })
@@ -25,10 +12,9 @@ export class User extends BaseEntity {
@Property({ unique: true }) @Property({ unique: true })
phone!: string; phone!: string;
@Enum({ @Property({ default: true })
items: () => UserStatus, isActive: boolean = true;
nullable: true,
default: UserStatus.pending, @OneToMany(() => RefreshToken, token => token.user)
}) refreshTokens = new Collection<RefreshToken>(this);
status?: UserStatus;
} }
+11 -17
View File
@@ -1,7 +1,7 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@mikro-orm/nestjs'; import { InjectRepository } from '@mikro-orm/nestjs';
import { EntityRepository, FilterQuery } from '@mikro-orm/core'; import { EntityRepository, FilterQuery } from '@mikro-orm/core';
import { User, UserStatus } from './entities/user.entity'; import { User } from './entities/user.entity';
import { EntityManager } from '@mikro-orm/postgresql'; import { EntityManager } from '@mikro-orm/postgresql';
import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserDto } from './dto/update-user.dto';
import { FindUsersDto } from './dto/find-user.dto'; import { FindUsersDto } from './dto/find-user.dto';
@@ -28,23 +28,23 @@ export class UserService {
return user; return user;
} }
async updateUser(userId: number, dto: UpdateUserDto): Promise<User> { // async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
const user = await this.em.findOneOrFail(User, userId, {}).catch(() => { // const user = await this.em.findOneOrFail(User, userId, {}).catch(() => {
throw new NotFoundException(`User with ID ${userId} not found.`); // throw new NotFoundException(`User with ID ${userId} not found.`);
}); // });
this.em.assign(user, { ...dto, status: UserStatus.finished }); // this.em.assign(user, { ...dto });
await this.em.flush(); // await this.em.flush();
return user; // return user;
} // }
async findByPhone(phone: string): Promise<User | null> { async findByPhone(phone: string): Promise<User | null> {
return this.userRepository.findOne({ phone }); return this.userRepository.findOne({ phone });
} }
async findById(id: number): Promise<User | null> { async findById(id: string): Promise<User | null> {
return this.userRepository.findOne({ id }); return this.userRepository.findOne({ id });
} }
@@ -55,7 +55,7 @@ export class UserService {
} }
async findAll(dto: FindUsersDto): Promise<PaginatedResult<User>> { async findAll(dto: FindUsersDto): Promise<PaginatedResult<User>> {
const { page = 1, limit = 10, search, education, orderBy = 'createdAt', order = 'desc' } = dto; const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto;
// 1. Calculate pagination // 1. Calculate pagination
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
@@ -63,10 +63,6 @@ export class UserService {
// 2. Build the 'where' filter query // 2. Build the 'where' filter query
const where: FilterQuery<User> = {}; const where: FilterQuery<User> = {};
if (education) {
where.education = education;
}
// 4. Add 'search' logic (case-insensitive) // 4. Add 'search' logic (case-insensitive)
if (search) { if (search) {
const searchPattern = `%${search}%`; const searchPattern = `%${search}%`;
@@ -75,8 +71,6 @@ export class UserService {
{ firstName: { $ilike: searchPattern } }, { firstName: { $ilike: searchPattern } },
{ lastName: { $ilike: searchPattern } }, { lastName: { $ilike: searchPattern } },
{ phone: { $ilike: searchPattern } }, { phone: { $ilike: searchPattern } },
{ nationalCode: { $ilike: searchPattern } },
{ personalCode: { $ilike: searchPattern } },
]; ];
} }