Sead amin

This commit is contained in:
2026-07-06 13:35:20 +03:30
parent 74d5b7a768
commit bfc077df1c
8 changed files with 190 additions and 0 deletions
+2
View File
@@ -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,
+18
View File
@@ -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)
+28
View File
@@ -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<void> {
const user = await this.usersService.findByMobile(mobile);
if (!user || user.role !== UserRole.ADMIN) {
throw new BadRequestException('این شماره ادمین نیست');
}
}
private async registerSalonOwner(
dto: VerifySalonOtpDto,
): Promise<SalonAuthResponse> {
@@ -303,6 +319,18 @@ export class AuthService {
return this.issueTokens(user);
}
async verifyAdminOtp(dto: VerifyAdminOtpDto): Promise<AuthTokensResponse> {
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<AuthTokensResponse> {
const storedTokens = await this.refreshTokenRepository.find({
where: { revokedAt: IsNull() },
+10
View File
@@ -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;
}
+11
View File
@@ -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 {}
@@ -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<void> {
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<void> {
for (const mobile of ADMIN_MOBILES) {
await queryRunner.query(`DELETE FROM users WHERE mobile = $1`, [mobile]);
}
}
}
+58
View File
@@ -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<User>,
@InjectRepository(Customer)
private readonly customersRepository: Repository<Customer>,
) {}
async onModuleInit(): Promise<void> {
await this.seedAdminUsers();
}
private async seedAdminUsers(): Promise<void> {
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}`,
);
}
}
}
}
+12
View File
@@ -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);