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 { 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],
|
||||||
|
|||||||
@@ -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,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({
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 {
|
export interface ILocalTokenPayload {
|
||||||
id: string;
|
id: string;
|
||||||
role: RoleEnum;
|
|
||||||
wildduckUserId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConsoleTokenPayload {
|
export interface IConsoleTokenPayload {
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 { 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 })
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 } },
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user