diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 0f99f01..58b895c 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -20,6 +20,7 @@ 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 { 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 { VerifyOtpDto } from './dto/verify-otp.dto'; @@ -63,7 +64,7 @@ export class AuthController { @Public() @ApiPublicEndpoint('Request OTP code via SMS') - @ApiStandardMessageResponse('OTP sent') + @ApiStandardOkResponse(RequestOtpResponseDto) @ApiStandardTooManyRequestsResponse() @Post('otp/request') requestOtp(@Body() dto: RequestOtpDto) { diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index e4ff4b7..63e0995 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -48,7 +48,6 @@ export class AuthService { private readonly otpSalonSignupExpiresInMinutes: number; private readonly otpMaxAttempts: number; private readonly otpCooldownSeconds: number; - private readonly otpDebug: boolean; private readonly refreshExpiresIn: string; constructor( @@ -86,14 +85,13 @@ export class AuthService { this.configService.get('OTP_COOLDOWN_SECONDS') ?? '60', 10, ); - this.otpDebug = this.configService.get('OTP_DEBUG') === 'true'; this.refreshExpiresIn = this.configService.get('JWT_REFRESH_EXPIRES_IN') ?? '7d'; } - async requestOtp(mobile: string): Promise<{ message: string }> { - await this.sendOtp(mobile, this.otpExpiresInMinutes); - return { message: 'OTP sent' }; + async requestOtp(mobile: string): Promise<{ message: string; code?: string }> { + const code = await this.sendOtp(mobile, this.otpExpiresInMinutes); + return this.buildOtpRequestResponse(code); } async checkSalonMobile( @@ -108,6 +106,7 @@ export class AuthService { message: string; isRegistered: boolean; expiresInSeconds: number; + code?: string; }> { await this.ensureSalonMobileCanProceed(mobile); const isRegistered = await this.isSalonOwnerRegistered(mobile); @@ -115,10 +114,10 @@ export class AuthService { ? this.otpExpiresInMinutes : this.otpSalonSignupExpiresInMinutes; - await this.sendOtp(mobile, expiresInMinutes); + const code = await this.sendOtp(mobile, expiresInMinutes); return { - message: 'OTP sent', + ...this.buildOtpRequestResponse(code), isRegistered, expiresInSeconds: expiresInMinutes * 60, }; @@ -145,7 +144,7 @@ export class AuthService { private async sendOtp( mobile: string, expiresInMinutes: number, - ): Promise { + ): Promise { const recentOtp = await this.otpRepository.findOne({ where: { mobile, verified: false }, order: { createdAt: 'DESC' }, @@ -176,16 +175,29 @@ export class AuthService { const sent = await this.smsService.sendSms(mobile, `کد ورود: ${code}`); - if (!sent) { + if (!sent && !this.shouldExposeOtpInResponse()) { throw new BadRequestException('Failed to send OTP'); } - if ( - this.otpDebug && - this.configService.get('NODE_ENV') === 'development' - ) { + if (this.shouldExposeOtpInResponse()) { this.logger.debug(`OTP for ${mobile}: ${code}`); } + + return code; + } + + private shouldExposeOtpInResponse(): boolean { + return this.configService.get('NODE_ENV') !== 'production'; + } + + private buildOtpRequestResponse(code: string): { + message: string; + code?: string; + } { + return { + message: 'OTP sent', + ...(this.shouldExposeOtpInResponse() ? { code } : {}), + }; } private async consumeOtp(mobile: string, code: string): Promise { diff --git a/src/auth/dto/request-otp-response.dto.ts b/src/auth/dto/request-otp-response.dto.ts new file mode 100644 index 0000000..e554c22 --- /dev/null +++ b/src/auth/dto/request-otp-response.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class RequestOtpResponseDto { + @ApiProperty({ example: 'OTP sent' }) + message: string; + + @ApiPropertyOptional({ + description: 'Only returned in non-production environments when SMS is unavailable', + example: '12345', + }) + code?: string; +} diff --git a/src/auth/dto/request-salon-otp-response.dto.ts b/src/auth/dto/request-salon-otp-response.dto.ts index e54b390..f7e87cc 100644 --- a/src/auth/dto/request-salon-otp-response.dto.ts +++ b/src/auth/dto/request-salon-otp-response.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class RequestSalonOtpResponseDto { @ApiProperty({ example: 'OTP sent' }) @@ -14,4 +14,10 @@ export class RequestSalonOtpResponseDto { example: 900, }) expiresInSeconds: number; + + @ApiPropertyOptional({ + description: 'Only returned in non-production environments when SMS is unavailable', + example: '12345', + }) + code?: string; }