Sead amin
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user