From 2432a20b45f6877af00ab85992ab34d44d8a518b Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Fri, 10 Apr 2026 18:11:49 +0700 Subject: [PATCH] feat(api): add async error handling to critical module handlers Wrap async operations at application layer boundaries with proper try/catch, LoggerService logging, and domain exceptions: - UploadMediaHandler: mediaStorage.upload() error boundary - ExportUserDataHandler: Promise.all() error logging - ForceDeleteUserHandler: $transaction error logging - LoginUserHandler: token generation error boundary - RefreshTokenHandler: token rotation error boundary - CreatePaymentHandler: payment gateway call error boundary Co-Authored-By: Paperclip --- .../export-user-data.handler.ts | 52 +++++++++++++------ .../force-delete-user.handler.ts | 11 +++- .../commands/login-user/login-user.handler.ts | 25 ++++++--- .../refresh-token/refresh-token.handler.ts | 16 +++++- .../upload-media/upload-media.handler.ts | 25 ++++++--- .../create-payment/create-payment.handler.ts | 25 ++++++--- 6 files changed, 114 insertions(+), 40 deletions(-) diff --git a/apps/api/src/modules/auth/application/commands/export-user-data/export-user-data.handler.ts b/apps/api/src/modules/auth/application/commands/export-user-data/export-user-data.handler.ts index 0b0d329..6e999e7 100644 --- a/apps/api/src/modules/auth/application/commands/export-user-data/export-user-data.handler.ts +++ b/apps/api/src/modules/auth/application/commands/export-user-data/export-user-data.handler.ts @@ -41,23 +41,41 @@ export class ExportUserDataHandler implements ICommandHandler { - constructor(private readonly tokenService: TokenService) {} + constructor( + private readonly tokenService: TokenService, + private readonly logger: LoggerService, + ) {} async execute(command: LoginUserCommand): Promise { - return this.tokenService.generateTokenPair({ - sub: command.userId, - phone: command.phone, - role: command.role, - }); + try { + return await this.tokenService.generateTokenPair({ + sub: command.userId, + phone: command.phone, + role: command.role, + }); + } catch (error) { + this.logger.error( + `Token generation failed for user ${command.userId}: ${error instanceof Error ? error.message : error}`, + error instanceof Error ? error.stack : undefined, + 'LoginUserHandler', + ); + throw new UnauthorizedException('Không thể tạo phiên đăng nhập, vui lòng thử lại'); + } } } diff --git a/apps/api/src/modules/auth/application/commands/refresh-token/refresh-token.handler.ts b/apps/api/src/modules/auth/application/commands/refresh-token/refresh-token.handler.ts index 02f17b8..674f192 100644 --- a/apps/api/src/modules/auth/application/commands/refresh-token/refresh-token.handler.ts +++ b/apps/api/src/modules/auth/application/commands/refresh-token/refresh-token.handler.ts @@ -1,6 +1,6 @@ import { Inject } from '@nestjs/common'; import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs'; -import { UnauthorizedException } from '@modules/shared'; +import { type LoggerService, UnauthorizedException } from '@modules/shared'; import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository'; import { type TokenService, type TokenPair } from '../../../infrastructure/services/token.service'; import { RefreshTokenCommand } from './refresh-token.command'; @@ -10,10 +10,22 @@ export class RefreshTokenHandler implements ICommandHandler constructor( private readonly tokenService: TokenService, @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository, + private readonly logger: LoggerService, ) {} async execute(command: RefreshTokenCommand): Promise { - const rotated = await this.tokenService.rotateRefreshToken(command.refreshToken); + let rotated: Awaited>; + try { + rotated = await this.tokenService.rotateRefreshToken(command.refreshToken); + } catch (error) { + this.logger.error( + `Token rotation failed: ${error instanceof Error ? error.message : error}`, + error instanceof Error ? error.stack : undefined, + 'RefreshTokenHandler', + ); + throw new UnauthorizedException('Không thể làm mới phiên đăng nhập'); + } + if (!rotated) { throw new UnauthorizedException('Refresh token không hợp lệ hoặc đã hết hạn'); } diff --git a/apps/api/src/modules/listings/application/commands/upload-media/upload-media.handler.ts b/apps/api/src/modules/listings/application/commands/upload-media/upload-media.handler.ts index a8b424e..9a757d7 100644 --- a/apps/api/src/modules/listings/application/commands/upload-media/upload-media.handler.ts +++ b/apps/api/src/modules/listings/application/commands/upload-media/upload-media.handler.ts @@ -1,7 +1,7 @@ import { Inject } from '@nestjs/common'; import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs'; import { createId } from '@paralleldrive/cuid2'; -import { NotFoundException, ValidationException } from '@modules/shared'; +import { type LoggerService, NotFoundException, ValidationException } from '@modules/shared'; import { PropertyMediaEntity } from '../../../domain/entities/property-media.entity'; import { PROPERTY_REPOSITORY, type IPropertyRepository } from '../../../domain/repositories/property.repository'; import { MEDIA_STORAGE_SERVICE, type IMediaStorageService } from '../../../infrastructure/services/media-storage.service'; @@ -14,6 +14,7 @@ export class UploadMediaHandler implements ICommandHandler { constructor( @Inject(PROPERTY_REPOSITORY) private readonly propertyRepo: IPropertyRepository, @Inject(MEDIA_STORAGE_SERVICE) private readonly mediaStorage: IMediaStorageService, + private readonly logger: LoggerService, ) {} async execute(command: UploadMediaCommand): Promise<{ mediaId: string; url: string }> { @@ -29,12 +30,22 @@ export class UploadMediaHandler implements ICommandHandler { const mediaType = command.file.mimetype.startsWith('video/') ? 'video' as const : 'image' as const; - const url = await this.mediaStorage.upload( - command.file.buffer, - command.file.originalname, - command.file.mimetype, - `properties/${command.propertyId}`, - ); + let url: string; + try { + url = await this.mediaStorage.upload( + command.file.buffer, + command.file.originalname, + command.file.mimetype, + `properties/${command.propertyId}`, + ); + } catch (error) { + this.logger.error( + `Media upload failed for property ${command.propertyId}: ${error instanceof Error ? error.message : error}`, + error instanceof Error ? error.stack : undefined, + 'UploadMediaHandler', + ); + throw new ValidationException('Tải lên media thất bại, vui lòng thử lại'); + } const mediaId = createId(); const media = PropertyMediaEntity.createNew( diff --git a/apps/api/src/modules/payments/application/commands/create-payment/create-payment.handler.ts b/apps/api/src/modules/payments/application/commands/create-payment/create-payment.handler.ts index 6fea201..424bc19 100644 --- a/apps/api/src/modules/payments/application/commands/create-payment/create-payment.handler.ts +++ b/apps/api/src/modules/payments/application/commands/create-payment/create-payment.handler.ts @@ -65,13 +65,24 @@ export class CreatePaymentHandler implements ICommandHandler