This commit is contained in:
2025-11-10 12:39:41 +03:30
parent 06f814b331
commit 45ca2fde06
24 changed files with 794 additions and 586 deletions
+18 -1
View File
@@ -33,9 +33,11 @@
"cache-manager": "^7.2.4",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"dayjs": "^1.11.19",
"fastify": "^5.6.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"ulid": "^3.0.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
@@ -7344,6 +7346,12 @@
"integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==",
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -13389,6 +13397,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ulid": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ulid/-/ulid-3.0.1.tgz",
"integrity": "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q==",
"license": "MIT",
"bin": {
"ulid": "dist/cli.js"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+3 -1
View File
@@ -46,9 +46,11 @@
"cache-manager": "^7.2.4",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"dayjs": "^1.11.19",
"fastify": "^5.6.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"ulid": "^3.0.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
+2 -2
View File
@@ -2,7 +2,7 @@ import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import dataBaseConfig from './config/mikro-orm.config';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UserModule } from './modules/user/user.module';
import { UserModule } from './modules/users/user.module';
// import { CacheModule } from '@nestjs/cache-manager';
// import { cacheConfig } from './config/cache.config';
import { UtilsModule } from './modules/utils/utils.module';
@@ -42,7 +42,7 @@ import { SubmitionModule } from './modules/submition/submition.module';
limit: 5, // max requests per window
},
]),
SubmitionModule
SubmitionModule,
],
controllers: [],
// providers: [CacheService],
+21
View File
@@ -0,0 +1,21 @@
import { PrimaryKey, Property, sql, OptionalProps } from '@mikro-orm/core';
export abstract class BaseEntity {
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt' | 'deletedAt';
@PrimaryKey({ type: 'string', columnType: 'char(26)' })
id: string = ulid();
@Property({ default: sql.now(), type: 'timestamptz' })
createdAt: Date = new Date();
@Property({
onUpdate: () => new Date(),
defaultRaw: 'now()',
columnType: 'timestamptz',
})
updatedAt: Date = new Date();
@Property({ nullable: true })
deletedAt?: Date;
}
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
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 -1
View File
@@ -1,5 +1,5 @@
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthService } from './services/auth.service';
import { RequestOtpDto } from './dto/request-otp.dto';
import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger';
import { VerifyOtpDto } from './dto/verify-otp.dto copy';
+2 -2
View File
@@ -1,8 +1,8 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthService } from './services/auth.service';
import { AuthController } from './auth.controller';
import { UtilsModule } from '../utils/utils.module';
import { UserModule } from '../user/user.module';
import { UserModule } from '../users/user.module';
import { JwtModule } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { AdminModule } from '../admin/admin.module';
+15
View File
@@ -0,0 +1,15 @@
import { RoleEnum } from "../../users/enums/role.enum";
export interface ILocalTokenPayload {
id: string;
role: RoleEnum;
wildduckUserId: string;
}
export interface IConsoleTokenPayload {
id: string;
permissions?: string[];
isAdmin?: boolean;
}
export interface ITokenPayload extends ILocalTokenPayload, IConsoleTokenPayload {}
@@ -1,12 +1,12 @@
import { Injectable, BadRequestException, UnauthorizedException, NotFoundException } from '@nestjs/common';
import { RequestOtpDto } from './dto/request-otp.dto';
import { CacheService } from '../utils/cache.service';
import { SmsService } from '../utils/sms.service';
import { UserService } from '../user/user.service';
import { RequestOtpDto } from '../dto/request-otp.dto';
import { CacheService } from '../../utils/cache.service';
import { SmsService } from '../../utils/sms.service';
import { UserService } from '../../users/user.service';
import { randomInt } from 'crypto';
import { JwtService } from '@nestjs/jwt';
import { AdminService } from '../admin/admin.service';
import { AuthEntityType } from 'src/common/guards/auth.guard';
import { AdminService } from '../../admin/admin.service';
import { AuthEntityType } from 'src/modules/auth/guards/auth.guard';
@Injectable()
export class AuthService {
+154
View File
@@ -0,0 +1,154 @@
import { randomBytes } from 'node:crypto';
import { EntityManager } from '@mikro-orm/postgresql';
import { BadRequestException, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import dayjs from 'dayjs';
import { AuthMessage, UserMessage } from '../../../common/enums/message.enum';
import { RefreshToken } from '../../users/entities/refresh-token.entity';
import { User } from '../../users/entities/user.entity';
import { ITokenPayload } from '../interfaces/IToken-payload';
@Injectable()
export class TokensService {
private readonly logger = new Logger(TokensService.name);
constructor(
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
private readonly em: EntityManager,
) {}
async generateTokens(user: User, em?: EntityManager) {
return this.generateAccessAndRefreshToken(
{
id: user.id,
role: user.role.name,
},
em,
);
}
private async generateAccessAndRefreshToken(payload: ITokenPayload, em?: EntityManager) {
const accessExpire = this.configService.getOrThrow<number>('ACCESS_TOKEN_EXPIRE');
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const accessToken = await this.jwtService.signAsync(payload, { expiresIn: `${accessExpire}m` });
const refreshToken = await this.generateRefreshToken();
await this.storeRefreshToken(payload.id, refreshToken, em);
return {
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'minute').valueOf() },
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() },
};
}
async storeRefreshToken(userId: string, refreshToken: string, em?: EntityManager) {
const entityManager = em || this.em.fork();
const isExternalTransaction = !!em && em.isInTransaction();
try {
// Only start transaction if no external EntityManager with transaction is provided
if (!isExternalTransaction && !entityManager.isInTransaction()) {
await entityManager.begin();
}
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const expiresAt = dayjs().add(refreshExpire, 'day').toDate();
const user = await entityManager.findOne(User, { id: userId });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
const token = entityManager.create(RefreshToken, {
token: refreshToken,
user,
expiresAt,
});
// Use persist instead of persistAndFlush when in transaction
if (isExternalTransaction) {
await entityManager.persist(token);
} else {
await entityManager.persistAndFlush(token);
if (entityManager.isInTransaction()) {
await entityManager.commit();
}
}
} catch (error) {
this.logger.error(error);
// Only rollback if we started the transaction
if (!isExternalTransaction && entityManager.isInTransaction()) {
await entityManager.rollback();
}
throw error;
}
}
async refreshToken(oldRefreshToken: string) {
const entityManager = this.em.fork();
try {
await entityManager.begin();
const token = await entityManager.findOne(
RefreshToken,
{ token: oldRefreshToken },
{
populate: ['user', 'user.role'],
},
);
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
if (dayjs(token.expiresAt).isBefore(dayjs())) {
await entityManager.remove(token);
await entityManager.flush();
await entityManager.commit();
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
// Remove the old token
await entityManager.remove(token);
// Generate new tokens within the same transaction
const tokens = await this.generateTokens(token.user, entityManager);
// Commit the transaction
await entityManager.flush();
await entityManager.commit();
return tokens;
} catch (error) {
this.logger.error(error);
if (entityManager.isInTransaction()) {
await entityManager.rollback();
}
throw error;
}
}
async invalidateRefreshToken(userId: string) {
const entityManager = this.em.fork();
try {
await entityManager.begin();
await entityManager.nativeDelete(RefreshToken, { user: { id: userId } });
this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`);
await entityManager.commit();
} catch (error) {
this.logger.error(error);
if (entityManager.isInTransaction()) {
await entityManager.rollback();
}
throw error;
}
}
private async generateRefreshToken() {
return randomBytes(32).toString('hex');
}
}
@@ -1,5 +1,5 @@
import { Entity, Enum, PrimaryKey, Property, OptionalProps, ManyToOne } from '@mikro-orm/core';
import { User } from '../../user/entities/user.entity';
import { User } from '../../users/entities/user.entity';
export enum SubmitionStatus {
finished = 'تکمیل',
@@ -13,11 +13,11 @@ import {
Delete,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody, ApiParam } from '@nestjs/swagger';
import { AuthGuard, type AuthRequest } from 'src/common/guards/auth.guard';
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/common/guards/auth.admin.guard';
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';
+1 -1
View File
@@ -4,7 +4,7 @@ import { ApiBody, ApiConsumes, ApiOperation, ApiBearerAuth } from '@nestjs/swagg
import { UploadMultipleFileDto, UploadSingleFileDto } from './DTO/upload-file.dto';
import { UploaderService } from './providers/uploader.service';
import { AuthGuard } from 'src/common/guards/auth.guard';
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
@Controller('uploader')
export class UploaderController {
-68
View File
@@ -1,68 +0,0 @@
import { Entity, Enum, PrimaryKey, Property, OptionalProps, OneToMany, Collection } from '@mikro-orm/core';
import { Submition } from '../../submition/entities/submition.entity';
export enum EducationLevel {
diploma = 'دیپلم',
associate = 'کاردانی',
bachelor = 'کارشناسی',
master = 'کارشناسی ارشد',
phd = 'دکتری',
}
export enum UserStatus {
finished = 'finished',
pending = 'pending',
}
@Entity({ tableName: 'users' })
export class User {
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt';
@OneToMany(() => Submition, submition => submition.user)
submissions = new Collection<Submition>(this);
@PrimaryKey()
id!: number;
@Property({ nullable: true })
firstName?: string;
@Property({ nullable: true })
lastName?: string;
@Property({ nullable: true })
fatherName?: string;
@Property({ unique: true })
phone!: string;
@Property({ nullable: true })
isMale?: boolean;
@Property({ unique: true, nullable: true })
nationalCode?: string;
@Property({ nullable: true })
personalCode?: string;
@Property({ nullable: true })
hireType?: string;
@Property({ nullable: true })
workLocation?: string;
@Property({ default: 0, nullable: true })
workExperienceYear?: number;
@Enum({ items: () => EducationLevel, nullable: true })
education?: EducationLevel;
@Enum({ items: () => UserStatus, nullable: true, default: UserStatus.pending })
status?: UserStatus;
@Property({ defaultRaw: 'now()', columnType: 'timestamptz' })
createdAt: Date = new Date();
@Property({ onUpdate: () => new Date(), defaultRaw: 'now()', columnType: 'timestamptz' })
updatedAt: Date = new Date();
}
@@ -0,0 +1,19 @@
import { Entity, ManyToOne, Opt, Property } from "@mikro-orm/core";
import { User } from "./user.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
@Entity()
export class RefreshToken extends BaseEntity {
@Property({ type: "varchar", length: 255 })
token!: string;
@ManyToOne(() => User, { deleteRule: "cascade" })
user!: User;
@Property({ type: "timestamptz" })
expiresAt!: Date;
@Property({ default: false })
isRevoked: boolean & Opt;
}
+34
View File
@@ -0,0 +1,34 @@
import { Entity, Enum, Property, Filter, BaseEntity } from '@mikro-orm/core';
export enum EducationLevel {
diploma = 'دیپلم',
associate = 'کاردانی',
bachelor = 'کارشناسی',
master = 'کارشناسی ارشد',
phd = 'دکتری',
}
export enum UserStatus {
finished = 'finished',
pending = 'pending',
}
@Filter({ name: 'notDeleted', cond: { deletedAt: null }, default: true })
@Entity({ tableName: 'users' })
export class User extends BaseEntity {
@Property({ nullable: true })
firstName?: string;
@Property({ nullable: true })
lastName?: string;
@Property({ unique: true })
phone!: string;
@Enum({
items: () => UserStatus,
nullable: true,
default: UserStatus.pending,
})
status?: UserStatus;
}
@@ -1,10 +1,10 @@
import { Controller, Get, Patch, Body, Req, UseGuards, Query, ValidationPipe } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody } from '@nestjs/swagger';
import { AuthGuard, type AuthRequest } from 'src/common/guards/auth.guard';
import { AuthGuard, type AuthRequest } from 'src/modules/auth/guards/auth.guard';
import { UserService } from './user.service';
import { UpdateUserDto } from './dto/update-user.dto';
import { FindUsersDto } from './dto/find-user.dto';
import { AdminAuthGuard } from 'src/common/guards/auth.admin.guard';
import { AdminAuthGuard } from 'src/modules/auth/guards/auth.admin.guard';
@ApiTags('User')
@Controller('user')