change flow
This commit is contained in:
@@ -16,9 +16,14 @@ import { AuthService } from './auth.service';
|
|||||||
import { CurrentUser } from './decorators/current-user.decorator';
|
import { CurrentUser } from './decorators/current-user.decorator';
|
||||||
import { Public } from './decorators/public.decorator';
|
import { Public } from './decorators/public.decorator';
|
||||||
import { AuthTokensResponseDto } from './dto/auth-tokens-response.dto';
|
import { AuthTokensResponseDto } from './dto/auth-tokens-response.dto';
|
||||||
|
import { CheckSalonMobileDto } from './dto/check-salon-mobile.dto';
|
||||||
|
import { CheckSalonMobileResponseDto } from './dto/check-salon-mobile-response.dto';
|
||||||
import { RefreshTokenDto } from './dto/refresh-token.dto';
|
import { RefreshTokenDto } from './dto/refresh-token.dto';
|
||||||
import { RequestOtpDto } from './dto/request-otp.dto';
|
import { RequestOtpDto } from './dto/request-otp.dto';
|
||||||
|
import { RequestSalonOtpResponseDto } from './dto/request-salon-otp-response.dto';
|
||||||
|
import { SalonAuthResponseDto } from './dto/salon-auth-response.dto';
|
||||||
import { VerifyOtpDto } from './dto/verify-otp.dto';
|
import { VerifyOtpDto } from './dto/verify-otp.dto';
|
||||||
|
import { VerifySalonOtpDto } from './dto/verify-salon-otp.dto';
|
||||||
import { AuthUser } from './interfaces/auth-user.interface';
|
import { AuthUser } from './interfaces/auth-user.interface';
|
||||||
|
|
||||||
@ApiTags('Auth')
|
@ApiTags('Auth')
|
||||||
@@ -27,6 +32,35 @@ import { AuthUser } from './interfaces/auth-user.interface';
|
|||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private readonly authService: AuthService) {}
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@ApiPublicEndpoint('Check whether a salon-owner account exists for a mobile')
|
||||||
|
@ApiStandardOkResponse(CheckSalonMobileResponseDto)
|
||||||
|
@Post('salon/check-mobile')
|
||||||
|
checkSalonMobile(@Body() dto: CheckSalonMobileDto) {
|
||||||
|
return this.authService.checkSalonMobile(dto.mobile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@ApiPublicEndpoint(
|
||||||
|
'Send OTP for salon login/signup. Signup OTP expires later (15m default). Call this after the signup form is filled.',
|
||||||
|
)
|
||||||
|
@ApiStandardOkResponse(RequestSalonOtpResponseDto)
|
||||||
|
@ApiStandardTooManyRequestsResponse()
|
||||||
|
@Post('salon/otp/request')
|
||||||
|
requestSalonOtp(@Body() dto: RequestOtpDto) {
|
||||||
|
return this.authService.requestSalonOtp(dto.mobile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@ApiPublicEndpoint(
|
||||||
|
'Verify salon OTP. Existing owners only need mobile+code. New owners must also send owner and salon fields.',
|
||||||
|
)
|
||||||
|
@ApiStandardOkResponse(SalonAuthResponseDto)
|
||||||
|
@Post('salon/otp/verify')
|
||||||
|
verifySalonOtp(@Body() dto: VerifySalonOtpDto) {
|
||||||
|
return this.authService.verifySalonOtp(dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@ApiPublicEndpoint('Request OTP code via SMS')
|
@ApiPublicEndpoint('Request OTP code via SMS')
|
||||||
@ApiStandardMessageResponse('OTP sent')
|
@ApiStandardMessageResponse('OTP sent')
|
||||||
@@ -37,7 +71,9 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@ApiPublicEndpoint('Verify OTP and receive access/refresh tokens')
|
@ApiPublicEndpoint(
|
||||||
|
'Verify OTP and receive access/refresh tokens. New users must send role (customer or salon-owner).',
|
||||||
|
)
|
||||||
@ApiStandardOkResponse(AuthTokensResponseDto)
|
@ApiStandardOkResponse(AuthTokensResponseDto)
|
||||||
@Post('otp/verify')
|
@Post('otp/verify')
|
||||||
verifyOtp(@Body() dto: VerifyOtpDto) {
|
verifyOtp(@Body() dto: VerifyOtpDto) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { AuthorizationModule } from '../authorization/authorization.module';
|
import { AuthorizationModule } from '../authorization/authorization.module';
|
||||||
import { Customer } from '../customers/entities/customer.entity';
|
import { Customer } from '../customers/entities/customer.entity';
|
||||||
|
import { Salon } from '../salons/entities/salon.entity';
|
||||||
import { SmsModule } from '../sms/sms.module';
|
import { SmsModule } from '../sms/sms.module';
|
||||||
import { UsersModule } from '../users/users.module';
|
import { UsersModule } from '../users/users.module';
|
||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from './auth.controller';
|
||||||
@@ -17,7 +18,7 @@ import { JwtStrategy } from './strategies/jwt.strategy';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Otp, RefreshToken, Customer]),
|
TypeOrmModule.forFeature([Otp, RefreshToken, Customer, Salon]),
|
||||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
|
|||||||
+153
-18
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
|
ConflictException,
|
||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Injectable,
|
Injectable,
|
||||||
@@ -12,11 +13,13 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { Repository, IsNull } from 'typeorm';
|
import { Repository, IsNull } from 'typeorm';
|
||||||
import { Customer } from '../customers/entities/customer.entity';
|
import { Customer } from '../customers/entities/customer.entity';
|
||||||
|
import { Salon } from '../salons/entities/salon.entity';
|
||||||
import { SmsService } from '../sms/sms.service';
|
import { SmsService } from '../sms/sms.service';
|
||||||
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 { VerifyOtpDto } from './dto/verify-otp.dto';
|
import { VerifyOtpDto } from './dto/verify-otp.dto';
|
||||||
|
import { VerifySalonOtpDto } from './dto/verify-salon-otp.dto';
|
||||||
import { Otp } from './entities/otp.entity';
|
import { Otp } from './entities/otp.entity';
|
||||||
import { RefreshToken } from './entities/refresh-token.entity';
|
import { RefreshToken } from './entities/refresh-token.entity';
|
||||||
import { JwtPayload } from './interfaces/jwt-payload.interface';
|
import { JwtPayload } from './interfaces/jwt-payload.interface';
|
||||||
@@ -33,11 +36,16 @@ export interface AuthTokensResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SalonAuthResponse extends AuthTokensResponse {
|
||||||
|
salon?: Salon;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private readonly logger = new Logger(AuthService.name);
|
private readonly logger = new Logger(AuthService.name);
|
||||||
private readonly otpLength: number;
|
private readonly otpLength: number;
|
||||||
private readonly otpExpiresInMinutes: number;
|
private readonly otpExpiresInMinutes: number;
|
||||||
|
private readonly otpSalonSignupExpiresInMinutes: number;
|
||||||
private readonly otpMaxAttempts: number;
|
private readonly otpMaxAttempts: number;
|
||||||
private readonly otpCooldownSeconds: number;
|
private readonly otpCooldownSeconds: number;
|
||||||
private readonly otpDebug: boolean;
|
private readonly otpDebug: boolean;
|
||||||
@@ -50,6 +58,8 @@ export class AuthService {
|
|||||||
private readonly refreshTokenRepository: Repository<RefreshToken>,
|
private readonly refreshTokenRepository: Repository<RefreshToken>,
|
||||||
@InjectRepository(Customer)
|
@InjectRepository(Customer)
|
||||||
private readonly customersRepository: Repository<Customer>,
|
private readonly customersRepository: Repository<Customer>,
|
||||||
|
@InjectRepository(Salon)
|
||||||
|
private readonly salonsRepository: Repository<Salon>,
|
||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
private readonly smsService: SmsService,
|
private readonly smsService: SmsService,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
@@ -63,6 +73,11 @@ export class AuthService {
|
|||||||
this.configService.get<string>('OTP_EXPIRES_IN_MINUTES') ?? '5',
|
this.configService.get<string>('OTP_EXPIRES_IN_MINUTES') ?? '5',
|
||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
|
this.otpSalonSignupExpiresInMinutes = parseInt(
|
||||||
|
this.configService.get<string>('OTP_SALON_SIGNUP_EXPIRES_IN_MINUTES') ??
|
||||||
|
'15',
|
||||||
|
10,
|
||||||
|
);
|
||||||
this.otpMaxAttempts = parseInt(
|
this.otpMaxAttempts = parseInt(
|
||||||
this.configService.get<string>('OTP_MAX_ATTEMPTS') ?? '5',
|
this.configService.get<string>('OTP_MAX_ATTEMPTS') ?? '5',
|
||||||
10,
|
10,
|
||||||
@@ -77,6 +92,60 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async requestOtp(mobile: string): Promise<{ message: string }> {
|
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({
|
const recentOtp = await this.otpRepository.findOne({
|
||||||
where: { mobile, verified: false },
|
where: { mobile, verified: false },
|
||||||
order: { createdAt: 'DESC' },
|
order: { createdAt: 'DESC' },
|
||||||
@@ -97,9 +166,7 @@ export class AuthService {
|
|||||||
|
|
||||||
const code = this.generateOtpCode();
|
const code = this.generateOtpCode();
|
||||||
const hashedCode = await bcrypt.hash(code, 10);
|
const hashedCode = await bcrypt.hash(code, 10);
|
||||||
const expiresAt = new Date(
|
const expiresAt = new Date(Date.now() + expiresInMinutes * 60 * 1000);
|
||||||
Date.now() + this.otpExpiresInMinutes * 60 * 1000,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.otpRepository.save({
|
await this.otpRepository.save({
|
||||||
mobile,
|
mobile,
|
||||||
@@ -119,13 +186,11 @@ export class AuthService {
|
|||||||
) {
|
) {
|
||||||
this.logger.debug(`OTP for ${mobile}: ${code}`);
|
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({
|
const otp = await this.otpRepository.findOne({
|
||||||
where: { mobile: dto.mobile, verified: false },
|
where: { mobile, verified: false },
|
||||||
order: { createdAt: 'DESC' },
|
order: { createdAt: 'DESC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,7 +206,7 @@ export class AuthService {
|
|||||||
throw new UnauthorizedException('Too many failed attempts');
|
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) {
|
if (!isValid) {
|
||||||
otp.attempts += 1;
|
otp.attempts += 1;
|
||||||
await this.otpRepository.save(otp);
|
await this.otpRepository.save(otp);
|
||||||
@@ -150,6 +215,67 @@ export class AuthService {
|
|||||||
|
|
||||||
otp.verified = true;
|
otp.verified = true;
|
||||||
await this.otpRepository.save(otp);
|
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);
|
const user = await this.resolveUserForLogin(dto);
|
||||||
return this.issueTokens(user);
|
return this.issueTokens(user);
|
||||||
@@ -205,27 +331,36 @@ export class AuthService {
|
|||||||
return existingUser;
|
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 firstName = dto.firstName ?? 'کاربر';
|
||||||
const lastName = dto.lastName ?? dto.mobile.slice(-4);
|
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({
|
const user = await this.usersService.create({
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
mobile: dto.mobile,
|
mobile: dto.mobile,
|
||||||
role: UserRole.CUSTOMER,
|
role: dto.role!,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.customersRepository.save({
|
if (dto.role === UserRole.CUSTOMER) {
|
||||||
userId: user.id,
|
const dateOfBirth = dto.dateOfBirth ?? '2000-01-01';
|
||||||
dateOfBirth,
|
const address = dto.address ?? '-';
|
||||||
address,
|
|
||||||
});
|
await this.customersRepository.save({
|
||||||
|
userId: user.id,
|
||||||
|
dateOfBirth,
|
||||||
|
address,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class CheckSalonMobileResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Whether a salon-owner account exists for this mobile',
|
||||||
|
})
|
||||||
|
isRegistered: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { Matches } from 'class-validator';
|
||||||
|
|
||||||
|
export class CheckSalonMobileDto {
|
||||||
|
@Matches(/^09\d{9}$/)
|
||||||
|
mobile: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class RequestSalonOtpResponseDto {
|
||||||
|
@ApiProperty({ example: 'OTP sent' })
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Whether a salon-owner account exists for this mobile',
|
||||||
|
})
|
||||||
|
isRegistered: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'OTP validity window in seconds',
|
||||||
|
example: 900,
|
||||||
|
})
|
||||||
|
expiresInSeconds: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Salon } from '../../salons/entities/salon.entity';
|
||||||
|
import { AuthTokensResponseDto } from './auth-tokens-response.dto';
|
||||||
|
|
||||||
|
export class SalonAuthResponseDto extends AuthTokensResponseDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
type: Salon,
|
||||||
|
description: 'Present only when a new salon account is created',
|
||||||
|
})
|
||||||
|
salon?: Salon;
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
IsDateString,
|
IsDateString,
|
||||||
|
IsIn,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
Length,
|
Length,
|
||||||
Matches,
|
Matches,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
import { UserRole } from '../../users/enums/user-role.enum';
|
||||||
|
|
||||||
|
export const OTP_SIGNUP_ROLES = [UserRole.CUSTOMER, UserRole.SALON] as const;
|
||||||
|
|
||||||
export class VerifyOtpDto {
|
export class VerifyOtpDto {
|
||||||
@Matches(/^09\d{9}$/)
|
@Matches(/^09\d{9}$/)
|
||||||
@@ -32,4 +36,8 @@ export class VerifyOtpDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@Length(1, 500)
|
@Length(1, 500)
|
||||||
address?: string;
|
address?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(OTP_SIGNUP_ROLES)
|
||||||
|
role?: (typeof OTP_SIGNUP_ROLES)[number];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
Length,
|
||||||
|
Matches,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class VerifySalonOtpDto {
|
||||||
|
@Matches(/^09\d{9}$/)
|
||||||
|
mobile: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Length(4, 6)
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 100)
|
||||||
|
firstName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 100)
|
||||||
|
lastName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 200)
|
||||||
|
salonName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
salonDescription?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 500)
|
||||||
|
salonAddress?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Matches(/^09\d{9}$/)
|
||||||
|
salonPhone?: string;
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ export default () => {
|
|||||||
'https://api.sms-webservice.com/api/V3',
|
'https://api.sms-webservice.com/api/V3',
|
||||||
OTP_LENGTH: process.env.OTP_LENGTH ?? '5',
|
OTP_LENGTH: process.env.OTP_LENGTH ?? '5',
|
||||||
OTP_EXPIRES_IN_MINUTES: process.env.OTP_EXPIRES_IN_MINUTES ?? '5',
|
OTP_EXPIRES_IN_MINUTES: process.env.OTP_EXPIRES_IN_MINUTES ?? '5',
|
||||||
|
OTP_SALON_SIGNUP_EXPIRES_IN_MINUTES:
|
||||||
|
process.env.OTP_SALON_SIGNUP_EXPIRES_IN_MINUTES ?? '15',
|
||||||
OTP_MAX_ATTEMPTS: process.env.OTP_MAX_ATTEMPTS ?? '5',
|
OTP_MAX_ATTEMPTS: process.env.OTP_MAX_ATTEMPTS ?? '5',
|
||||||
OTP_COOLDOWN_SECONDS: process.env.OTP_COOLDOWN_SECONDS ?? '60',
|
OTP_COOLDOWN_SECONDS: process.env.OTP_COOLDOWN_SECONDS ?? '60',
|
||||||
OTP_DEBUG: process.env.OTP_DEBUG ?? 'false',
|
OTP_DEBUG: process.env.OTP_DEBUG ?? 'false',
|
||||||
|
|||||||
Reference in New Issue
Block a user