diff --git a/src/app.module.ts b/src/app.module.ts index 4baf777..b262e41 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,12 +16,10 @@ import { OrderModule } from './modules/order/order.module'; import { NotificationsModule } from './modules/notification/notifications.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { CacheModule } from '@nestjs/cache-manager'; -import { cacheConfig } from './config/cache.config'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, cache: true }), - CacheModule.registerAsync(cacheConfig()), MikroOrmModule.forRootAsync(dataBaseConfig), UserModule, UtilsModule, diff --git a/src/common/enums/permission.enum.ts b/src/common/enums/permission.enum.ts index d931b67..7ae581e 100644 --- a/src/common/enums/permission.enum.ts +++ b/src/common/enums/permission.enum.ts @@ -8,6 +8,8 @@ export enum Permission { MANAGE_CATEGORIES = 'manage_categories', // Order Management + VIEW_ORDERS='view_orders', + ASSIGN_DESIGNER='assign_designer', MANAGE_ORDERS = 'manage_orders', // User & Admin Management @@ -47,4 +49,6 @@ export const PermissionTitles: Record = { [Permission.MANAGE_SETTINGS]: 'مدیریت تنظیمات', [Permission.MANAGE_REPORTS]: 'مدیریت گزارشات', [Permission.MANAGE_CONTACTS]: 'مدیریت تماس‌های کاربران', + [Permission.VIEW_ORDERS]: "", + [Permission.ASSIGN_DESIGNER]: "" }; diff --git a/src/config/cache.config.ts b/src/config/cache.config.ts deleted file mode 100755 index c2134ea..0000000 --- a/src/config/cache.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Keyv } from 'keyv'; -import KeyvRedis from '@keyv/redis'; -import type { CacheModuleAsyncOptions } from '@nestjs/cache-manager'; -import { ConfigService } from '@nestjs/config'; -import { CacheableMemory } from 'cacheable'; - -export function cacheConfig(): CacheModuleAsyncOptions { - return { - isGlobal: true, - inject: [ConfigService], - useFactory: async (configService: ConfigService) => { - const redisUri = configService.get('REDIS_URI'); - const namespace = configService.get('CACHE_NAMESPACE', 'app-cache'); - const ttl = +configService.get('CACHE_TTL', 3600); //1 hour - - return { - stores: [ - new Keyv({ - store: new CacheableMemory({ ttl: ttl * 1000, lruSize: 5000 }), - namespace: namespace - }), - new KeyvRedis(redisUri, { namespace: namespace }), - ], - }; - }, - }; -} \ No newline at end of file diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index e6c7277..b356e29 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -7,12 +7,13 @@ import { JwtModule } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { AdminModule } from '../admin/admin.module'; import { TokensService } from './services/tokens.service'; - import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { User } from '../user/entities/user.entity'; import { AdminAuthGuard } from './guards/adminAuth.guard'; import { RefreshToken } from './entities/refresh-token.entity'; import { NotificationsModule } from '../notification/notifications.module'; +import { OtpRepository } from './repository/otp.repository'; +import { OtpService } from './services/otp.service'; +import { Otp } from './entities/otp.entity'; @Module({ imports: [ @@ -33,13 +34,13 @@ import { NotificationsModule } from '../notification/notifications.module'; inject: [ConfigService], }), forwardRef(() => AdminModule), - - MikroOrmModule.forFeature([User, RefreshToken]), + + MikroOrmModule.forFeature([RefreshToken, Otp]), forwardRef(() => NotificationsModule), ], controllers: [AuthController], - providers: [AuthService, TokensService, AdminAuthGuard], + providers: [AuthService, TokensService, AdminAuthGuard, OtpRepository, OtpService], exports: [AdminAuthGuard], }) export class AuthModule { } diff --git a/src/modules/auth/entities/otp.entity.ts b/src/modules/auth/entities/otp.entity.ts new file mode 100644 index 0000000..4c78492 --- /dev/null +++ b/src/modules/auth/entities/otp.entity.ts @@ -0,0 +1,16 @@ +import { Entity, Index, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core'; + +@Entity({ tableName: 'otps' }) +@Index({ properties: ['phone'] }) +export class Otp { + [OptionalProps]?: 'createdAt' + + @PrimaryKey({ type: 'varchar', }) + phone!: string; + + @Property() + code!: string; + + @Property({ columnType: 'timestamptz' }) + expiresAt!: Date +} diff --git a/src/modules/auth/repository/otp.repository.ts b/src/modules/auth/repository/otp.repository.ts new file mode 100644 index 0000000..ff241ef --- /dev/null +++ b/src/modules/auth/repository/otp.repository.ts @@ -0,0 +1,14 @@ +import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; +import { Injectable } from '@nestjs/common'; +import { Otp } from '../entities/otp.entity'; + + +@Injectable() +export class OtpRepository extends EntityRepository { + constructor( + readonly em: EntityManager, + ) { + super(em, Otp); + } + +} diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index b06ad6c..7a48eb3 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -1,6 +1,5 @@ import { Injectable, BadRequestException } from '@nestjs/common'; import { RequestOtpDto } from '../dto/request-otp.dto'; -import { CacheService } from '../../util/cache.service'; import { SmsService } from '../../notification/services/sms.service'; import { UserService } from '../../user/providers/user.service'; import { randomInt } from 'crypto'; @@ -9,46 +8,35 @@ import { AuthMessage } from 'src/common/enums/message.enum'; import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; import { AdminLoginTransformer } from '../transformers/admin-login.transformer'; import { UserLoginTransformer } from '../transformers/user-login.transformer'; -import { ConfigService } from '@nestjs/config'; import { normalizePhone } from '../../util/phone.util'; +import { OtpService } from './otp.service'; @Injectable() export class AuthService { readonly ADMIN_PERMISSIONS_KEY = 'admin-perms'; - private OTP_EXPIRATION_TIME: number; constructor( - private readonly cacheService: CacheService, private readonly smsService: SmsService, private readonly userService: UserService, private readonly tokensService: TokensService, private readonly adminRepository: AdminRepository, - private readonly configService: ConfigService, + private readonly otpService: OtpService, ) { - this.OTP_EXPIRATION_TIME = this.configService.get('OTP_EXPIRATION_TIME') ?? 240; - } - - private userOtpKey(phone: string) { - return `otp:${phone}`; - } - - private adminOtpKey(phone: string) { - return `otp-admin:${phone}`; + } async requestOtp(dto: RequestOtpDto, isAdmin: boolean) { const { phone } = dto; const normalizedPhone = normalizePhone(phone); - + if (isAdmin) { await this.valdateAdmin(normalizedPhone) } const code = this.generateOtpCode(); - const key = isAdmin ? this.adminOtpKey(normalizedPhone) : this.userOtpKey(normalizedPhone); - - await this.cacheService.set(key, code, this.OTP_EXPIRATION_TIME); - + + await this.otpService.set(normalizedPhone, code) + // await this.smsService.sendotp(normalizedPhone, code); return { code }; @@ -56,16 +44,14 @@ export class AuthService { async verifyOtp(phone: string, code: string) { const normalizedPhone = normalizePhone(phone); - const key = this.userOtpKey(normalizedPhone); - const cachedCode = await this.cacheService.get(key); + + const cachedCode = await this.otpService.get(normalizedPhone); if (!cachedCode) throw new BadRequestException('OTP expired or not found'); if (cachedCode !== code) throw new BadRequestException('Invalid OTP'); - await this.cacheService.del(key); + await this.otpService.delete(normalizedPhone) const user = await this.userService.getOrCreate({ - firstName: '[کاربر]', - lastName: '', phone: normalizedPhone, gender: true, maxCredit: 0 @@ -81,13 +67,13 @@ export class AuthService { async verifyOtpAdmin(phone: string, code: string) { const normalizedPhone = normalizePhone(phone); - const key = this.adminOtpKey(normalizedPhone); - const cachedCode = await this.cacheService.get(key); + + const cachedCode = await this.otpService.get(normalizedPhone); if (!cachedCode) throw new BadRequestException('OTP expired or not found'); if (cachedCode !== code) throw new BadRequestException('Invalid OTP'); - await this.cacheService.del(key); + await this.otpService.delete(normalizedPhone); const admin = await this.adminRepository.findOne({ phone: normalizedPhone }, { populate: ['role', 'role.permissions'] }); diff --git a/src/modules/auth/services/otp.service.ts b/src/modules/auth/services/otp.service.ts new file mode 100644 index 0000000..91a709f --- /dev/null +++ b/src/modules/auth/services/otp.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from "@nestjs/common"; +import { OtpRepository } from "../repository/otp.repository"; +import { EntityManager } from "@mikro-orm/postgresql"; +import { ConfigService } from "@nestjs/config"; + +@Injectable() +export class OtpService { + private OTP_EXPIRATION_TIME: number;// in seconds + constructor( + private readonly otpRepository: OtpRepository, + private readonly configService: ConfigService, + private readonly em: EntityManager + ) { + this.OTP_EXPIRATION_TIME = this.configService.get('OTP_EXPIRATION_TIME') ?? 240; + } + + async set(phone: string, code: string) { + let otpRecord = await this.otpRepository.findOne({ phone }) + const expiresAt = new Date(Date.now() + this.OTP_EXPIRATION_TIME * 1000) + + if (!otpRecord) { + otpRecord = this.otpRepository.create({ + phone, + code, + expiresAt + }) + } else { + otpRecord.code = code + otpRecord.expiresAt = expiresAt + } + + await this.em.persistAndFlush(otpRecord) + + return { successs: true } + + } + + async get(phone: string) { + + let otpRecord = await this.otpRepository.findOne({ phone, expiresAt: { $gt: new Date() } }) + + return otpRecord?.code + + } + + async delete(phone: string) { + await this.otpRepository.nativeDelete({ phone }) + return { successs: true } + } +} \ No newline at end of file diff --git a/src/modules/auth/transformers/user-login.transformer.ts b/src/modules/auth/transformers/user-login.transformer.ts index 8f222f2..fdb22ff 100644 --- a/src/modules/auth/transformers/user-login.transformer.ts +++ b/src/modules/auth/transformers/user-login.transformer.ts @@ -2,7 +2,7 @@ import type { User } from '../../user/entities/user.entity'; export interface UserLoginResponse { id: string; - firstName: string; + firstName?: string; lastName?: string; phone: string; isActive?: boolean; diff --git a/src/modules/user/dto/create-user.dto.ts b/src/modules/user/dto/create-user.dto.ts index 1a6c3d6..83a133b 100644 --- a/src/modules/user/dto/create-user.dto.ts +++ b/src/modules/user/dto/create-user.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsBoolean, IsNotEmpty, IsMobilePhone, IsInt } from 'class-validator'; +import { IsString, IsBoolean, IsNotEmpty, IsMobilePhone, IsInt, IsNumber, IsOptional } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class CreateUserDto { @@ -10,13 +10,13 @@ export class CreateUserDto { @ApiProperty({ description: "User's first name", example: 'علی' }) @IsString() - @IsNotEmpty() - firstName: string; + @IsOptional() + firstName?: string; @ApiProperty({ description: "User's last name", example: 'جعفری' }) @IsString() - @IsNotEmpty() - lastName: string; + @IsOptional() + lastName?: string; @ApiProperty({ description: 'Gender flag (boolean)', example: true }) @IsBoolean() diff --git a/src/modules/user/entities/user.entity.ts b/src/modules/user/entities/user.entity.ts index 62eb63e..667ed49 100644 --- a/src/modules/user/entities/user.entity.ts +++ b/src/modules/user/entities/user.entity.ts @@ -13,8 +13,8 @@ export class User extends BaseEntity { @PrimaryKey({ type: 'string', columnType: 'char(26)' }) id: string = ulid(); - @Property() - firstName!: string; + @Property({ nullable: true }) + firstName?: string; @Property({ nullable: true }) lastName?: string; diff --git a/src/modules/util/cache.service.ts b/src/modules/util/cache.service.ts deleted file mode 100755 index b9e7a32..0000000 --- a/src/modules/util/cache.service.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager'; -import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'; - -@Injectable() -export class CacheService implements OnModuleInit { - private readonly logger = new Logger(CacheService.name); - - constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} - - async onModuleInit() { -; - this.logger.log(`Active Cache Store: ${JSON.stringify(this.cacheManager as any)}`); - - // Should log 'Keyv' or 'KeyvRedis'. If it says 'Memory', the config is still wrong. - this.logger.log('Pinging Redis to check connection...'); - try { - await this.set('ping', 'pong', 10); // TTL in seconds - const result = await this.get('ping'); - - if (result === 'pong') { - this.logger.log('✅ Redis connection successful (set/get test passed).'); - } else { - this.logger.error(`❌ Redis connection test failed. Expected 'pong', got '${result}'.`); - } - - await this.del('ping'); - } catch (error: any) { - this.logger.error('❌ Failed to connect to Redis during ping test:', error.message); - } - } - - /** Get value from cache */ - async get(key: string): Promise { - return await this.cacheManager.get(key); - } - - /** Set value in cache with TTL in seconds */ - async set(key: string, value: any, ttlSeconds: number) { - // cache-manager expects TTL in seconds - await this.cacheManager.set(key, value, ttlSeconds * 1000); - } - - /** Delete a key from cache */ - async del(key: string) { - await this.cacheManager.del(key); - } - - /** Get remaining TTL in seconds */ - async getTtl(key: string): Promise { - return await this.cacheManager.ttl(key); - } -} diff --git a/src/modules/util/utils.module.ts b/src/modules/util/utils.module.ts index 2f7cef5..ef5fdaf 100644 --- a/src/modules/util/utils.module.ts +++ b/src/modules/util/utils.module.ts @@ -1,11 +1,10 @@ import { Module } from '@nestjs/common'; -import { CacheService } from './cache.service'; @Module({ imports: [], controllers: [], - providers: [CacheService, ], - exports: [CacheService], + providers: [ ], + exports: [], }) export class UtilsModule {}