diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 457dd49..4f5b9db 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -46,6 +46,11 @@ import { AppController } from './app.controller'; ttl: 60_000, limit: 10, }, + { + name: 'payment-callback', + ttl: 60_000, + limit: 20, + }, ], }), ], diff --git a/apps/api/src/modules/payments/infrastructure/services/momo.service.ts b/apps/api/src/modules/payments/infrastructure/services/momo.service.ts index 7dffcda..b62e219 100644 --- a/apps/api/src/modules/payments/infrastructure/services/momo.service.ts +++ b/apps/api/src/modules/payments/infrastructure/services/momo.service.ts @@ -126,7 +126,10 @@ export class MomoService implements IPaymentGateway { .update(rawSignature) .digest('hex'); - const isValid = receivedSignature === expectedSignature; + const isValid = + receivedSignature.length > 0 && + receivedSignature.length === expectedSignature.length && + crypto.timingSafeEqual(Buffer.from(receivedSignature, 'hex'), Buffer.from(expectedSignature, 'hex')); const isSuccess = isValid && resultCode === '0'; this.logger.log( diff --git a/apps/api/src/modules/payments/infrastructure/services/vnpay.service.ts b/apps/api/src/modules/payments/infrastructure/services/vnpay.service.ts index 7fdadd2..eaa4a66 100644 --- a/apps/api/src/modules/payments/infrastructure/services/vnpay.service.ts +++ b/apps/api/src/modules/payments/infrastructure/services/vnpay.service.ts @@ -83,7 +83,10 @@ export class VnpayService implements IPaymentGateway { const hmac = crypto.createHmac('sha512', this.hashSecret); const checkSum = hmac.update(Buffer.from(signData, 'utf-8')).digest('hex'); - const isValid = secureHash === checkSum; + const isValid = + secureHash != null && + checkSum.length === secureHash.length && + crypto.timingSafeEqual(Buffer.from(secureHash, 'hex'), Buffer.from(checkSum, 'hex')); const responseCode = data['vnp_ResponseCode']; const isSuccess = isValid && responseCode === '00'; diff --git a/apps/api/src/modules/payments/infrastructure/services/zalopay.service.ts b/apps/api/src/modules/payments/infrastructure/services/zalopay.service.ts index 575ac54..2b019cb 100644 --- a/apps/api/src/modules/payments/infrastructure/services/zalopay.service.ts +++ b/apps/api/src/modules/payments/infrastructure/services/zalopay.service.ts @@ -104,7 +104,10 @@ export class ZalopayService implements IPaymentGateway { .update(dataStr) .digest('hex'); - const isValid = reqMac === mac; + const isValid = + reqMac.length > 0 && + reqMac.length === mac.length && + crypto.timingSafeEqual(Buffer.from(reqMac, 'hex'), Buffer.from(mac, 'hex')); let parsedData: Record = {}; let orderId = ''; diff --git a/apps/api/src/modules/payments/presentation/controllers/payments.controller.ts b/apps/api/src/modules/payments/presentation/controllers/payments.controller.ts index b3a9f56..0f082a8 100644 --- a/apps/api/src/modules/payments/presentation/controllers/payments.controller.ts +++ b/apps/api/src/modules/payments/presentation/controllers/payments.controller.ts @@ -8,6 +8,14 @@ import { Query, UseGuards, } from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiParam, +} from '@nestjs/swagger'; +import { Throttle } from '@nestjs/throttler'; import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { JwtAuthGuard } from '@modules/auth/presentation/guards/jwt-auth.guard'; import { RolesGuard } from '@modules/auth/presentation/guards/roles.guard'; @@ -29,6 +37,7 @@ import { RefundPaymentDto } from '../dto/refund-payment.dto'; import { ListTransactionsDto } from '../dto/list-transactions.dto'; import { type PaymentProvider } from '@prisma/client'; +@ApiTags('payments') @Controller('payments') export class PaymentsController { constructor( @@ -36,6 +45,11 @@ export class PaymentsController { private readonly queryBus: QueryBus, ) {} + @ApiBearerAuth() + @ApiOperation({ summary: 'Create a new payment' }) + @ApiResponse({ status: 201, description: 'Payment created successfully' }) + @ApiResponse({ status: 400, description: 'Bad request' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) @UseGuards(JwtAuthGuard) @Post() async createPayment( @@ -58,6 +72,10 @@ export class PaymentsController { ); } + @ApiOperation({ summary: 'Handle payment provider callback (webhook)' }) + @ApiResponse({ status: 201, description: 'Callback processed successfully' }) + @ApiParam({ name: 'provider', enum: ['vnpay', 'momo', 'zalopay'] }) + @Throttle({ 'payment-callback': { ttl: 60_000, limit: 20 } }) @Post('callback/:provider') async handleCallback( @Param('provider') provider: string, @@ -72,6 +90,11 @@ export class PaymentsController { ); } + @ApiBearerAuth() + @ApiOperation({ summary: 'Get payment status by ID' }) + @ApiResponse({ status: 200, description: 'Payment status retrieved' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 404, description: 'Payment not found' }) @UseGuards(JwtAuthGuard) @Get(':id') async getPaymentStatus( @@ -81,6 +104,10 @@ export class PaymentsController { return this.queryBus.execute(new GetPaymentStatusQuery(id, user.sub)); } + @ApiBearerAuth() + @ApiOperation({ summary: 'List transactions for the authenticated user' }) + @ApiResponse({ status: 200, description: 'Transactions retrieved' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) @UseGuards(JwtAuthGuard) @Get() async listTransactions( @@ -92,6 +119,12 @@ export class PaymentsController { ); } + @ApiBearerAuth() + @ApiOperation({ summary: 'Refund a payment (admin only)' }) + @ApiResponse({ status: 201, description: 'Refund initiated successfully' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden — admin role required' }) + @ApiResponse({ status: 404, description: 'Payment not found' }) @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN') @Post(':id/refund')