From bfc077df1c909ae928909196a8a8a2a148113fc3 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Mon, 6 Jul 2026 13:35:20 +0330 Subject: [PATCH] Sead amin --- src/app.module.ts | 2 + src/auth/auth.controller.ts | 18 ++++++ src/auth/auth.service.ts | 28 +++++++++ src/auth/dto/verify-admin-otp.dto.ts | 10 ++++ src/database/database.module.ts | 11 ++++ .../1720800000000-FixAdminUsersSeed.ts | 51 ++++++++++++++++ src/database/seed.service.ts | 58 +++++++++++++++++++ src/database/seeds/admin-users.seed.ts | 12 ++++ 8 files changed, 190 insertions(+) create mode 100644 src/auth/dto/verify-admin-otp.dto.ts create mode 100644 src/database/database.module.ts create mode 100644 src/database/migrations/1720800000000-FixAdminUsersSeed.ts create mode 100644 src/database/seed.service.ts create mode 100644 src/database/seeds/admin-users.seed.ts diff --git a/src/app.module.ts b/src/app.module.ts index 8e48558..44ef29b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AuthModule } from './auth/auth.module'; +import { DatabaseModule } from './database/database.module'; import { CustomersModule } from './customers/customers.module'; import configuration from './config/configuration'; import { getTypeOrmOptions } from './database/typeorm-options'; @@ -23,6 +24,7 @@ import { PaymentsModule } from './payments/payments.module'; load: [configuration], }), TypeOrmModule.forRoot(getTypeOrmOptions()), + DatabaseModule, AuthModule, UsersModule, SalonsModule, diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index d97fe36..ddc37ad 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -24,6 +24,7 @@ import { RequestOtpDto } from './dto/request-otp.dto'; import { RequestOtpResponseDto } from './dto/request-otp-response.dto'; import { RequestSalonOtpResponseDto } from './dto/request-salon-otp-response.dto'; import { SalonAuthResponseDto } from './dto/salon-auth-response.dto'; +import { VerifyAdminOtpDto } from './dto/verify-admin-otp.dto'; import { VerifyOtpDto } from './dto/verify-otp.dto'; import { VerifySalonOtpDto } from './dto/verify-salon-otp.dto'; import { AuthUser } from './interfaces/auth-user.interface'; @@ -72,6 +73,15 @@ export class AuthController { return this.authService.requestOtp(dto.mobile); } + @Public() + @ApiPublicEndpoint('Request OTP code for admin panel login') + @ApiStandardOkResponse(RequestOtpResponseDto) + @ApiStandardTooManyRequestsResponse() + @Post('admin/otp/request') + requestAdminOtp(@Body() dto: RequestOtpDto) { + return this.authService.requestAdminOtp(dto.mobile); + } + @Public() @ApiPublicEndpoint( 'Verify OTP and receive access/refresh tokens. New users must send role (customer or salon-owner).', @@ -82,6 +92,14 @@ export class AuthController { return this.authService.verifyOtp(dto); } + @Public() + @ApiPublicEndpoint('Verify admin OTP and receive access/refresh tokens') + @ApiStandardOkResponse(AuthTokensResponseDto) + @Post('admin/otp/verify') + verifyAdminOtp(@Body() dto: VerifyAdminOtpDto) { + return this.authService.verifyAdminOtp(dto); + } + @Public() @ApiPublicEndpoint('Refresh access token') @ApiStandardOkResponse(AuthTokensResponseDto) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 09618c9..bb07295 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -20,6 +20,7 @@ import { SalonSubscriptionsService } from '../subscriptions/salon-subscriptions. import { UserRole } from '../users/enums/user-role.enum'; import { User } from '../users/entities/user.entity'; import { UsersService } from '../users/users.service'; +import { VerifyAdminOtpDto } from './dto/verify-admin-otp.dto'; import { VerifyOtpDto } from './dto/verify-otp.dto'; import { VerifySalonOtpDto } from './dto/verify-salon-otp.dto'; import { Otp } from './entities/otp.entity'; @@ -104,6 +105,13 @@ export class AuthService { return this.buildOtpRequestResponse(code); } + async requestAdminOtp( + mobile: string, + ): Promise<{ message: string; code?: string }> { + await this.ensureAdminMobile(mobile); + return this.requestOtp(mobile); + } + async checkSalonMobile( mobile: string, ): Promise<{ isRegistered: boolean }> { @@ -254,6 +262,14 @@ export class AuthService { } } + private async ensureAdminMobile(mobile: string): Promise { + const user = await this.usersService.findByMobile(mobile); + + if (!user || user.role !== UserRole.ADMIN) { + throw new BadRequestException('این شماره ادمین نیست'); + } + } + private async registerSalonOwner( dto: VerifySalonOtpDto, ): Promise { @@ -303,6 +319,18 @@ export class AuthService { return this.issueTokens(user); } + async verifyAdminOtp(dto: VerifyAdminOtpDto): Promise { + await this.ensureAdminMobile(dto.mobile); + await this.consumeOtp(dto.mobile, dto.code); + + const user = await this.usersService.findByMobile(dto.mobile); + if (!user || user.role !== UserRole.ADMIN) { + throw new BadRequestException('این شماره ادمین نیست'); + } + + return this.issueTokens(user); + } + async refresh(refreshToken: string): Promise { const storedTokens = await this.refreshTokenRepository.find({ where: { revokedAt: IsNull() }, diff --git a/src/auth/dto/verify-admin-otp.dto.ts b/src/auth/dto/verify-admin-otp.dto.ts new file mode 100644 index 0000000..659d17c --- /dev/null +++ b/src/auth/dto/verify-admin-otp.dto.ts @@ -0,0 +1,10 @@ +import { IsString, Length, Matches } from 'class-validator'; + +export class VerifyAdminOtpDto { + @Matches(/^09\d{9}$/) + mobile: string; + + @IsString() + @Length(4, 6) + code: string; +} diff --git a/src/database/database.module.ts b/src/database/database.module.ts new file mode 100644 index 0000000..45688ea --- /dev/null +++ b/src/database/database.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Customer } from '../customers/entities/customer.entity'; +import { User } from '../users/entities/user.entity'; +import { SeedService } from './seed.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([User, Customer])], + providers: [SeedService], +}) +export class DatabaseModule {} diff --git a/src/database/migrations/1720800000000-FixAdminUsersSeed.ts b/src/database/migrations/1720800000000-FixAdminUsersSeed.ts new file mode 100644 index 0000000..eb93790 --- /dev/null +++ b/src/database/migrations/1720800000000-FixAdminUsersSeed.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +const ADMIN_MOBILES = ['09185290775', '09188630559']; + +export class FixAdminUsersSeed1720800000000 implements MigrationInterface { + name = 'FixAdminUsersSeed1720800000000'; + + public async up(queryRunner: QueryRunner): Promise { + for (const mobile of ADMIN_MOBILES) { + await queryRunner.query( + ` + DELETE FROM customers + WHERE "userId" IN ( + SELECT id FROM users WHERE mobile = $1 + ) + `, + [mobile], + ); + + const existingUsers: Array<{ id: string }> = await queryRunner.query( + `SELECT id FROM users WHERE mobile = $1`, + [mobile], + ); + + if (existingUsers.length > 0) { + await queryRunner.query( + `UPDATE users SET role = 'admin' WHERE mobile = $1`, + [mobile], + ); + continue; + } + + const firstName = mobile === '09185290775' ? 'hamid' : 'ادمین'; + const lastName = mobile === '09185290775' ? 'Zarghami' : 'گرو'; + + await queryRunner.query( + ` + INSERT INTO users ("firstName", "lastName", mobile, role) + VALUES ($1, $2, $3, 'admin') + `, + [firstName, lastName, mobile], + ); + } + } + + public async down(queryRunner: QueryRunner): Promise { + for (const mobile of ADMIN_MOBILES) { + await queryRunner.query(`DELETE FROM users WHERE mobile = $1`, [mobile]); + } + } +} diff --git a/src/database/seed.service.ts b/src/database/seed.service.ts new file mode 100644 index 0000000..e52672a --- /dev/null +++ b/src/database/seed.service.ts @@ -0,0 +1,58 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Customer } from '../customers/entities/customer.entity'; +import { User } from '../users/entities/user.entity'; +import { UserRole } from '../users/enums/user-role.enum'; +import { ADMIN_USERS_SEED } from './seeds/admin-users.seed'; + +@Injectable() +export class SeedService implements OnModuleInit { + private readonly logger = new Logger(SeedService.name); + + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + @InjectRepository(Customer) + private readonly customersRepository: Repository, + ) {} + + async onModuleInit(): Promise { + await this.seedAdminUsers(); + } + + private async seedAdminUsers(): Promise { + for (const adminSeed of ADMIN_USERS_SEED) { + const existingUser = await this.usersRepository.findOneBy({ + mobile: adminSeed.mobile, + }); + + if (!existingUser) { + await this.usersRepository.save({ + firstName: adminSeed.firstName, + lastName: adminSeed.lastName, + mobile: adminSeed.mobile, + role: UserRole.ADMIN, + }); + this.logger.log(`Created admin user for ${adminSeed.mobile}`); + continue; + } + + if (existingUser.role === UserRole.CUSTOMER) { + await this.customersRepository.delete({ userId: existingUser.id }); + existingUser.role = UserRole.ADMIN; + await this.usersRepository.save(existingUser); + this.logger.log( + `Converted customer ${adminSeed.mobile} to admin and removed customer profile`, + ); + continue; + } + + if (existingUser.role !== UserRole.ADMIN) { + this.logger.warn( + `Admin seed mobile ${adminSeed.mobile} is registered as ${existingUser.role}`, + ); + } + } + } +} diff --git a/src/database/seeds/admin-users.seed.ts b/src/database/seeds/admin-users.seed.ts new file mode 100644 index 0000000..5bd70ad --- /dev/null +++ b/src/database/seeds/admin-users.seed.ts @@ -0,0 +1,12 @@ +export interface AdminUserSeed { + mobile: string; + firstName: string; + lastName: string; +} + +export const ADMIN_USERS_SEED: AdminUserSeed[] = [ + { mobile: '09185290775', firstName: 'hamid', lastName: 'Zarghami' }, + { mobile: '09188630559', firstName: 'abolfazl', lastName: 'faraji' }, +]; + +export const ADMIN_MOBILES = ADMIN_USERS_SEED.map((admin) => admin.mobile);