Sead amin
This commit is contained in:
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { DatabaseModule } from './database/database.module';
|
||||||
import { CustomersModule } from './customers/customers.module';
|
import { CustomersModule } from './customers/customers.module';
|
||||||
import configuration from './config/configuration';
|
import configuration from './config/configuration';
|
||||||
import { getTypeOrmOptions } from './database/typeorm-options';
|
import { getTypeOrmOptions } from './database/typeorm-options';
|
||||||
@@ -23,6 +24,7 @@ import { PaymentsModule } from './payments/payments.module';
|
|||||||
load: [configuration],
|
load: [configuration],
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forRoot(getTypeOrmOptions()),
|
TypeOrmModule.forRoot(getTypeOrmOptions()),
|
||||||
|
DatabaseModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
SalonsModule,
|
SalonsModule,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { RequestOtpDto } from './dto/request-otp.dto';
|
|||||||
import { RequestOtpResponseDto } from './dto/request-otp-response.dto';
|
import { RequestOtpResponseDto } from './dto/request-otp-response.dto';
|
||||||
import { RequestSalonOtpResponseDto } from './dto/request-salon-otp-response.dto';
|
import { RequestSalonOtpResponseDto } from './dto/request-salon-otp-response.dto';
|
||||||
import { SalonAuthResponseDto } from './dto/salon-auth-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 { VerifyOtpDto } from './dto/verify-otp.dto';
|
||||||
import { VerifySalonOtpDto } from './dto/verify-salon-otp.dto';
|
import { VerifySalonOtpDto } from './dto/verify-salon-otp.dto';
|
||||||
import { AuthUser } from './interfaces/auth-user.interface';
|
import { AuthUser } from './interfaces/auth-user.interface';
|
||||||
@@ -72,6 +73,15 @@ export class AuthController {
|
|||||||
return this.authService.requestOtp(dto.mobile);
|
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()
|
@Public()
|
||||||
@ApiPublicEndpoint(
|
@ApiPublicEndpoint(
|
||||||
'Verify OTP and receive access/refresh tokens. New users must send role (customer or salon-owner).',
|
'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);
|
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()
|
@Public()
|
||||||
@ApiPublicEndpoint('Refresh access token')
|
@ApiPublicEndpoint('Refresh access token')
|
||||||
@ApiStandardOkResponse(AuthTokensResponseDto)
|
@ApiStandardOkResponse(AuthTokensResponseDto)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { SalonSubscriptionsService } from '../subscriptions/salon-subscriptions.
|
|||||||
import { UserRole } from '../users/enums/user-role.enum';
|
import { UserRole } from '../users/enums/user-role.enum';
|
||||||
import { User } from '../users/entities/user.entity';
|
import { User } from '../users/entities/user.entity';
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from '../users/users.service';
|
||||||
|
import { VerifyAdminOtpDto } from './dto/verify-admin-otp.dto';
|
||||||
import { VerifyOtpDto } from './dto/verify-otp.dto';
|
import { VerifyOtpDto } from './dto/verify-otp.dto';
|
||||||
import { VerifySalonOtpDto } from './dto/verify-salon-otp.dto';
|
import { VerifySalonOtpDto } from './dto/verify-salon-otp.dto';
|
||||||
import { Otp } from './entities/otp.entity';
|
import { Otp } from './entities/otp.entity';
|
||||||
@@ -104,6 +105,13 @@ export class AuthService {
|
|||||||
return this.buildOtpRequestResponse(code);
|
return this.buildOtpRequestResponse(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async requestAdminOtp(
|
||||||
|
mobile: string,
|
||||||
|
): Promise<{ message: string; code?: string }> {
|
||||||
|
await this.ensureAdminMobile(mobile);
|
||||||
|
return this.requestOtp(mobile);
|
||||||
|
}
|
||||||
|
|
||||||
async checkSalonMobile(
|
async checkSalonMobile(
|
||||||
mobile: string,
|
mobile: string,
|
||||||
): Promise<{ isRegistered: boolean }> {
|
): 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(
|
private async registerSalonOwner(
|
||||||
dto: VerifySalonOtpDto,
|
dto: VerifySalonOtpDto,
|
||||||
): Promise<SalonAuthResponse> {
|
): Promise<SalonAuthResponse> {
|
||||||
@@ -303,6 +319,18 @@ export class AuthService {
|
|||||||
return this.issueTokens(user);
|
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> {
|
async refresh(refreshToken: string): Promise<AuthTokensResponse> {
|
||||||
const storedTokens = await this.refreshTokenRepository.find({
|
const storedTokens = await this.refreshTokenRepository.find({
|
||||||
where: { revokedAt: IsNull() },
|
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