change flow
This commit is contained in:
+153
-18
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
@@ -12,11 +13,13 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { Repository, IsNull } from 'typeorm';
|
||||
import { Customer } from '../customers/entities/customer.entity';
|
||||
import { Salon } from '../salons/entities/salon.entity';
|
||||
import { SmsService } from '../sms/sms.service';
|
||||
import { UserRole } from '../users/enums/user-role.enum';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { VerifyOtpDto } from './dto/verify-otp.dto';
|
||||
import { VerifySalonOtpDto } from './dto/verify-salon-otp.dto';
|
||||
import { Otp } from './entities/otp.entity';
|
||||
import { RefreshToken } from './entities/refresh-token.entity';
|
||||
import { JwtPayload } from './interfaces/jwt-payload.interface';
|
||||
@@ -33,11 +36,16 @@ export interface AuthTokensResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export interface SalonAuthResponse extends AuthTokensResponse {
|
||||
salon?: Salon;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
private readonly otpLength: number;
|
||||
private readonly otpExpiresInMinutes: number;
|
||||
private readonly otpSalonSignupExpiresInMinutes: number;
|
||||
private readonly otpMaxAttempts: number;
|
||||
private readonly otpCooldownSeconds: number;
|
||||
private readonly otpDebug: boolean;
|
||||
@@ -50,6 +58,8 @@ export class AuthService {
|
||||
private readonly refreshTokenRepository: Repository<RefreshToken>,
|
||||
@InjectRepository(Customer)
|
||||
private readonly customersRepository: Repository<Customer>,
|
||||
@InjectRepository(Salon)
|
||||
private readonly salonsRepository: Repository<Salon>,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly smsService: SmsService,
|
||||
private readonly jwtService: JwtService,
|
||||
@@ -63,6 +73,11 @@ export class AuthService {
|
||||
this.configService.get<string>('OTP_EXPIRES_IN_MINUTES') ?? '5',
|
||||
10,
|
||||
);
|
||||
this.otpSalonSignupExpiresInMinutes = parseInt(
|
||||
this.configService.get<string>('OTP_SALON_SIGNUP_EXPIRES_IN_MINUTES') ??
|
||||
'15',
|
||||
10,
|
||||
);
|
||||
this.otpMaxAttempts = parseInt(
|
||||
this.configService.get<string>('OTP_MAX_ATTEMPTS') ?? '5',
|
||||
10,
|
||||
@@ -77,6 +92,60 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async requestOtp(mobile: string): Promise<{ message: string }> {
|
||||
await this.sendOtp(mobile, this.otpExpiresInMinutes);
|
||||
return { message: 'OTP sent' };
|
||||
}
|
||||
|
||||
async checkSalonMobile(
|
||||
mobile: string,
|
||||
): Promise<{ isRegistered: boolean }> {
|
||||
await this.ensureSalonMobileCanProceed(mobile);
|
||||
const isRegistered = await this.isSalonOwnerRegistered(mobile);
|
||||
return { isRegistered };
|
||||
}
|
||||
|
||||
async requestSalonOtp(mobile: string): Promise<{
|
||||
message: string;
|
||||
isRegistered: boolean;
|
||||
expiresInSeconds: number;
|
||||
}> {
|
||||
await this.ensureSalonMobileCanProceed(mobile);
|
||||
const isRegistered = await this.isSalonOwnerRegistered(mobile);
|
||||
const expiresInMinutes = isRegistered
|
||||
? this.otpExpiresInMinutes
|
||||
: this.otpSalonSignupExpiresInMinutes;
|
||||
|
||||
await this.sendOtp(mobile, expiresInMinutes);
|
||||
|
||||
return {
|
||||
message: 'OTP sent',
|
||||
isRegistered,
|
||||
expiresInSeconds: expiresInMinutes * 60,
|
||||
};
|
||||
}
|
||||
|
||||
async verifySalonOtp(dto: VerifySalonOtpDto): Promise<SalonAuthResponse> {
|
||||
await this.consumeOtp(dto.mobile, dto.code);
|
||||
|
||||
const existingUser = await this.usersService.findByMobile(dto.mobile);
|
||||
|
||||
if (existingUser) {
|
||||
if (existingUser.role !== UserRole.SALON) {
|
||||
throw new ConflictException(
|
||||
'This mobile is already registered with another account type',
|
||||
);
|
||||
}
|
||||
|
||||
return this.issueTokens(existingUser);
|
||||
}
|
||||
|
||||
return this.registerSalonOwner(dto);
|
||||
}
|
||||
|
||||
private async sendOtp(
|
||||
mobile: string,
|
||||
expiresInMinutes: number,
|
||||
): Promise<void> {
|
||||
const recentOtp = await this.otpRepository.findOne({
|
||||
where: { mobile, verified: false },
|
||||
order: { createdAt: 'DESC' },
|
||||
@@ -97,9 +166,7 @@ export class AuthService {
|
||||
|
||||
const code = this.generateOtpCode();
|
||||
const hashedCode = await bcrypt.hash(code, 10);
|
||||
const expiresAt = new Date(
|
||||
Date.now() + this.otpExpiresInMinutes * 60 * 1000,
|
||||
);
|
||||
const expiresAt = new Date(Date.now() + expiresInMinutes * 60 * 1000);
|
||||
|
||||
await this.otpRepository.save({
|
||||
mobile,
|
||||
@@ -119,13 +186,11 @@ export class AuthService {
|
||||
) {
|
||||
this.logger.debug(`OTP for ${mobile}: ${code}`);
|
||||
}
|
||||
|
||||
return { message: 'OTP sent' };
|
||||
}
|
||||
|
||||
async verifyOtp(dto: VerifyOtpDto): Promise<AuthTokensResponse> {
|
||||
private async consumeOtp(mobile: string, code: string): Promise<void> {
|
||||
const otp = await this.otpRepository.findOne({
|
||||
where: { mobile: dto.mobile, verified: false },
|
||||
where: { mobile, verified: false },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
@@ -141,7 +206,7 @@ export class AuthService {
|
||||
throw new UnauthorizedException('Too many failed attempts');
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(dto.code, otp.code);
|
||||
const isValid = await bcrypt.compare(code, otp.code);
|
||||
if (!isValid) {
|
||||
otp.attempts += 1;
|
||||
await this.otpRepository.save(otp);
|
||||
@@ -150,6 +215,67 @@ export class AuthService {
|
||||
|
||||
otp.verified = true;
|
||||
await this.otpRepository.save(otp);
|
||||
}
|
||||
|
||||
private async isSalonOwnerRegistered(mobile: string): Promise<boolean> {
|
||||
const user = await this.usersService.findByMobile(mobile);
|
||||
return user?.role === UserRole.SALON;
|
||||
}
|
||||
|
||||
private async ensureSalonMobileCanProceed(mobile: string): Promise<void> {
|
||||
const user = await this.usersService.findByMobile(mobile);
|
||||
|
||||
if (user && user.role !== UserRole.SALON) {
|
||||
throw new ConflictException(
|
||||
'This mobile is already registered with another account type',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async registerSalonOwner(
|
||||
dto: VerifySalonOtpDto,
|
||||
): Promise<SalonAuthResponse> {
|
||||
this.validateSalonSignupFields(dto);
|
||||
|
||||
const user = await this.usersService.create({
|
||||
firstName: dto.firstName!,
|
||||
lastName: dto.lastName!,
|
||||
mobile: dto.mobile,
|
||||
role: UserRole.SALON,
|
||||
});
|
||||
|
||||
const salon = await this.salonsRepository.save({
|
||||
name: dto.salonName!,
|
||||
description: dto.salonDescription ?? null,
|
||||
address: dto.salonAddress!,
|
||||
phone: dto.salonPhone!,
|
||||
ownerId: user.id,
|
||||
});
|
||||
|
||||
const tokens = await this.issueTokens(user);
|
||||
return { ...tokens, salon };
|
||||
}
|
||||
|
||||
private validateSalonSignupFields(dto: VerifySalonOtpDto): void {
|
||||
const requiredFields: Array<keyof VerifySalonOtpDto> = [
|
||||
'firstName',
|
||||
'lastName',
|
||||
'salonName',
|
||||
'salonAddress',
|
||||
'salonPhone',
|
||||
];
|
||||
|
||||
const missingFields = requiredFields.filter((field) => !dto[field]);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`Missing required fields for salon registration: ${missingFields.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyOtp(dto: VerifyOtpDto): Promise<AuthTokensResponse> {
|
||||
await this.consumeOtp(dto.mobile, dto.code);
|
||||
|
||||
const user = await this.resolveUserForLogin(dto);
|
||||
return this.issueTokens(user);
|
||||
@@ -205,27 +331,36 @@ export class AuthService {
|
||||
return existingUser;
|
||||
}
|
||||
|
||||
return this.createCustomerUser(dto);
|
||||
if (!dto.role) {
|
||||
throw new BadRequestException(
|
||||
'role is required when registering a new account',
|
||||
);
|
||||
}
|
||||
|
||||
return this.createUserFromOtp(dto);
|
||||
}
|
||||
|
||||
private async createCustomerUser(dto: VerifyOtpDto): Promise<User> {
|
||||
private async createUserFromOtp(dto: VerifyOtpDto): Promise<User> {
|
||||
const firstName = dto.firstName ?? 'کاربر';
|
||||
const lastName = dto.lastName ?? dto.mobile.slice(-4);
|
||||
const dateOfBirth = dto.dateOfBirth ?? '2000-01-01';
|
||||
const address = dto.address ?? '-';
|
||||
|
||||
const user = await this.usersService.create({
|
||||
firstName,
|
||||
lastName,
|
||||
mobile: dto.mobile,
|
||||
role: UserRole.CUSTOMER,
|
||||
role: dto.role!,
|
||||
});
|
||||
|
||||
await this.customersRepository.save({
|
||||
userId: user.id,
|
||||
dateOfBirth,
|
||||
address,
|
||||
});
|
||||
if (dto.role === UserRole.CUSTOMER) {
|
||||
const dateOfBirth = dto.dateOfBirth ?? '2000-01-01';
|
||||
const address = dto.address ?? '-';
|
||||
|
||||
await this.customersRepository.save({
|
||||
userId: user.id,
|
||||
dateOfBirth,
|
||||
address,
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user