update
This commit is contained in:
@@ -14,7 +14,6 @@ import { UploaderModule } from './modules/uploader/uploader.module';
|
||||
import { AdminModule } from './modules/admin/admin.module';
|
||||
// import { CacheService } from './modules/utils/cache.service';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { SubmitionModule } from './modules/submition/submition.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -42,7 +41,6 @@ import { SubmitionModule } from './modules/submition/submition.module';
|
||||
limit: 5, // max requests per window
|
||||
},
|
||||
]),
|
||||
SubmitionModule,
|
||||
],
|
||||
controllers: [],
|
||||
// providers: [CacheService],
|
||||
|
||||
@@ -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 {
|
||||
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt' | 'deletedAt';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AdminService } from './admin.service';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Admin } from './entities/user.entity';
|
||||
import { Admin } from './entities/admin.entity';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
@Module({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import { EntityRepository } from '@mikro-orm/core';
|
||||
import { Admin } from './entities/user.entity';
|
||||
import { Admin } from './entities/admin.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
|
||||
@Injectable()
|
||||
@@ -16,13 +16,13 @@ export class AdminService {
|
||||
return this.adminRepository.findOne({ phone });
|
||||
}
|
||||
|
||||
async findById(id: number): Promise<Admin | null> {
|
||||
async findById(id: string): Promise<Admin | null> {
|
||||
return this.adminRepository.findOne({ id });
|
||||
}
|
||||
|
||||
async create(phone: string): Promise<Admin> {
|
||||
const user = this.adminRepository.create({ phone });
|
||||
await this.em.persistAndFlush(user);
|
||||
return user;
|
||||
}
|
||||
// async create(phone: string): Promise<Admin> {
|
||||
// const user = this.adminRepository.create({ phone });
|
||||
// await this.em.persistAndFlush(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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
id: string;
|
||||
role: RoleEnum;
|
||||
wildduckUserId: string;
|
||||
}
|
||||
|
||||
export interface IConsoleTokenPayload {
|
||||
|
||||
@@ -55,7 +55,7 @@ export class AuthService {
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ export class TokensService {
|
||||
return this.generateAccessAndRefreshToken(
|
||||
{
|
||||
id: user.id,
|
||||
role: user.role.name,
|
||||
},
|
||||
em,
|
||||
);
|
||||
@@ -96,21 +95,21 @@ export class TokensService {
|
||||
RefreshToken,
|
||||
{ token: oldRefreshToken },
|
||||
{
|
||||
populate: ['user', 'user.role'],
|
||||
populate: ['user'],
|
||||
},
|
||||
);
|
||||
|
||||
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
|
||||
|
||||
if (dayjs(token.expiresAt).isBefore(dayjs())) {
|
||||
await entityManager.remove(token);
|
||||
entityManager.remove(token);
|
||||
await entityManager.flush();
|
||||
await entityManager.commit();
|
||||
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
|
||||
}
|
||||
|
||||
// Remove the old token
|
||||
await entityManager.remove(token);
|
||||
entityManager.remove(token);
|
||||
|
||||
// Generate new tokens within the same transaction
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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 { User } from "./user.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { Entity, ManyToOne, Opt, Property } from '@mikro-orm/core';
|
||||
|
||||
import { User } from './user.entity';
|
||||
import { BaseEntity } from 'src/common/entities/base.entity';
|
||||
@Entity()
|
||||
export class RefreshToken extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 255 })
|
||||
@Property({ type: 'varchar', length: 255 })
|
||||
token!: string;
|
||||
|
||||
@ManyToOne(() => User, { deleteRule: "cascade" })
|
||||
@ManyToOne(() => User, { deleteRule: 'cascade' })
|
||||
user!: User;
|
||||
|
||||
@Property({ type: "timestamptz" })
|
||||
@Property({ type: 'timestamptz' })
|
||||
expiresAt!: Date;
|
||||
|
||||
@Property({ default: false })
|
||||
|
||||
@@ -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' })
|
||||
export class User extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
@@ -25,10 +12,9 @@ export class User extends BaseEntity {
|
||||
@Property({ unique: true })
|
||||
phone!: string;
|
||||
|
||||
@Enum({
|
||||
items: () => UserStatus,
|
||||
nullable: true,
|
||||
default: UserStatus.pending,
|
||||
})
|
||||
status?: UserStatus;
|
||||
@Property({ default: true })
|
||||
isActive: boolean = true;
|
||||
|
||||
@OneToMany(() => RefreshToken, token => token.user)
|
||||
refreshTokens = new Collection<RefreshToken>(this);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { User } from './entities/user.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { FindUsersDto } from './dto/find-user.dto';
|
||||
@@ -28,23 +28,23 @@ export class UserService {
|
||||
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.`);
|
||||
});
|
||||
// async updateUser(userId: string, 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 });
|
||||
// this.em.assign(user, { ...dto });
|
||||
|
||||
await this.em.flush();
|
||||
// await this.em.flush();
|
||||
|
||||
return user;
|
||||
}
|
||||
// return user;
|
||||
// }
|
||||
|
||||
async findByPhone(phone: string): Promise<User | null> {
|
||||
return this.userRepository.findOne({ phone });
|
||||
}
|
||||
|
||||
async findById(id: number): Promise<User | null> {
|
||||
async findById(id: string): Promise<User | null> {
|
||||
return this.userRepository.findOne({ id });
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
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
|
||||
const offset = (page - 1) * limit;
|
||||
@@ -63,10 +63,6 @@ export class UserService {
|
||||
// 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}%`;
|
||||
@@ -75,8 +71,6 @@ export class UserService {
|
||||
{ firstName: { $ilike: searchPattern } },
|
||||
{ lastName: { $ilike: searchPattern } },
|
||||
{ phone: { $ilike: searchPattern } },
|
||||
{ nationalCode: { $ilike: searchPattern } },
|
||||
{ personalCode: { $ilike: searchPattern } },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user