feat(auth): add phoneNumber to profile update with SMS OTP re-verify
TEC-2722 — PATCH /api/v1/auth/profile now accepts phoneNumber alongside fullName, avatarUrl, and email. Phone changes are deferred until the user confirms the SMS OTP via POST /api/v1/auth/profile/verify-phone, mirroring the existing email-change OTP flow. - Add PhoneChangeRequestedEvent + user.phone_change_otp SMS template - Add VerifyPhoneChangeHandler with Redis-backed 10-minute OTP - Re-check phone uniqueness at verify time to catch races - Extend unit tests for UpdateProfileHandler + add VerifyPhoneChangeHandler spec Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -16,9 +16,8 @@ import {
|
||||
EndpointRateLimit,
|
||||
EndpointRateLimitGuard,
|
||||
UnauthorizedException,
|
||||
ValidationException,
|
||||
} from '@modules/shared';
|
||||
import { GenerateKycUploadUrlsCommand, type KycFileRequest } from '../../application/commands/generate-kyc-upload-urls/generate-kyc-upload-urls.command';
|
||||
import { GenerateKycUploadUrlsCommand } from '../../application/commands/generate-kyc-upload-urls/generate-kyc-upload-urls.command';
|
||||
import { LoginUserCommand } from '../../application/commands/login-user/login-user.command';
|
||||
import { type LoginResult } from '../../application/commands/login-user/login-user.handler';
|
||||
import { RefreshTokenCommand } from '../../application/commands/refresh-token/refresh-token.command';
|
||||
@@ -29,6 +28,8 @@ import { type UpdateProfileResultDto } from '../../application/commands/update-p
|
||||
import { VerifyEmailChangeCommand } from '../../application/commands/verify-email-change/verify-email-change.command';
|
||||
import { type VerifyEmailChangeResultDto } from '../../application/commands/verify-email-change/verify-email-change.handler';
|
||||
import { VerifyKycCommand } from '../../application/commands/verify-kyc/verify-kyc.command';
|
||||
import { VerifyPhoneChangeCommand } from '../../application/commands/verify-phone-change/verify-phone-change.command';
|
||||
import { type VerifyPhoneChangeResultDto } from '../../application/commands/verify-phone-change/verify-phone-change.handler';
|
||||
import { type AgentDto } from '../../application/queries/get-agent-by-user-id/get-agent-by-user-id.handler';
|
||||
import { GetAgentByUserIdQuery } from '../../application/queries/get-agent-by-user-id/get-agent-by-user-id.query';
|
||||
import { type UserProfileDto } from '../../application/queries/get-profile/get-profile.handler';
|
||||
@@ -37,12 +38,15 @@ import { type TokenService, type JwtPayload, type TokenPair } from '../../infras
|
||||
import { type LocalStrategyResult } from '../../infrastructure/strategies/local.strategy';
|
||||
import { CurrentUser } from '../decorators/current-user.decorator';
|
||||
import { Roles } from '../decorators/roles.decorator';
|
||||
import { type GenerateKycUploadUrlsDto } from '../dto/generate-kyc-upload-urls.dto';
|
||||
import { LoginDto } from '../dto/login.dto';
|
||||
import { type RefreshTokenDto } from '../dto/refresh-token.dto';
|
||||
import { type RegisterDto } from '../dto/register.dto';
|
||||
import { type SubmitKycDto } from '../dto/submit-kyc.dto';
|
||||
import { type UpdateProfileDto } from '../dto/update-profile.dto';
|
||||
import { type VerifyEmailChangeDto } from '../dto/verify-email-change.dto';
|
||||
import { type VerifyKycDto } from '../dto/verify-kyc.dto';
|
||||
import { type VerifyPhoneChangeDto } from '../dto/verify-phone-change.dto';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||
import { LocalAuthGuard } from '../guards/local-auth.guard';
|
||||
import { RolesGuard } from '../guards/roles.guard';
|
||||
@@ -227,11 +231,29 @@ export class AuthController {
|
||||
@Body() dto: UpdateProfileDto,
|
||||
): Promise<{ message: string; data: UpdateProfileResultDto }> {
|
||||
const result: UpdateProfileResultDto = await this.commandBus.execute(
|
||||
new UpdateProfileCommand(user.sub, dto.fullName, dto.avatarUrl, dto.email),
|
||||
new UpdateProfileCommand(user.sub, dto.fullName, dto.avatarUrl, dto.email, dto.phoneNumber),
|
||||
);
|
||||
return { message: 'Cập nhật hồ sơ thành công', data: result };
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('profile/verify-phone')
|
||||
@ApiBearerAuth('JWT')
|
||||
@ApiOperation({ summary: 'Verify phone number change with SMS OTP code' })
|
||||
@ApiResponse({ status: 201, description: 'Phone number changed successfully' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid or expired OTP code' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
@ApiResponse({ status: 409, description: 'Phone number already in use' })
|
||||
async verifyPhoneChange(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
@Body() dto: VerifyPhoneChangeDto,
|
||||
): Promise<{ message: string; data: VerifyPhoneChangeResultDto }> {
|
||||
const result: VerifyPhoneChangeResultDto = await this.commandBus.execute(
|
||||
new VerifyPhoneChangeCommand(user.sub, dto.code),
|
||||
);
|
||||
return { message: 'Số điện thoại đã được cập nhật thành công', data: result };
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('profile/verify-email')
|
||||
@ApiBearerAuth('JWT')
|
||||
@@ -268,7 +290,7 @@ export class AuthController {
|
||||
@ApiResponse({ status: 400, description: 'Validation error' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async generateKycUploadUrls(
|
||||
@Body() body: { files: KycFileRequest[] },
|
||||
@Body() body: GenerateKycUploadUrlsDto,
|
||||
@CurrentUser() user: JwtPayload,
|
||||
): Promise<{ field: string; uploadUrl: string; publicUrl: string; objectKey: string }[]> {
|
||||
return this.commandBus.execute(
|
||||
@@ -284,20 +306,9 @@ export class AuthController {
|
||||
@ApiResponse({ status: 400, description: 'Validation error' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async submitKyc(
|
||||
@Body()
|
||||
body: {
|
||||
documentType: string;
|
||||
documentNumber: string;
|
||||
frontImageUrl: string;
|
||||
backImageUrl?: string;
|
||||
selfieUrl?: string;
|
||||
},
|
||||
@Body() body: SubmitKycDto,
|
||||
@CurrentUser() user: JwtPayload,
|
||||
): Promise<{ message: string }> {
|
||||
if (!body.frontImageUrl) {
|
||||
throw new ValidationException('Vui lòng tải ảnh mặt trước giấy tờ');
|
||||
}
|
||||
|
||||
return this.commandBus.execute(
|
||||
new SubmitKycCommand(
|
||||
user.sub,
|
||||
|
||||
Reference in New Issue
Block a user