- Updated PLESK_DEPLOYMENT.md to include new Salesforce credentials and webhook security configurations. - Refactored order fulfillment controller to streamline the process and improve readability. - Introduced EnhancedWebhookSignatureGuard for improved HMAC signature validation and nonce management. - Updated various documentation files to reflect changes in endpoint naming from `/provision` to `/fulfill` for clarity and consistency. - Enhanced Redis integration for nonce storage to prevent replay attacks. - Removed deprecated WebhookSignatureGuard in favor of the new enhanced guard.
151 lines
5.0 KiB
TypeScript
151 lines
5.0 KiB
TypeScript
import { Controller, Post, Param, Body, Headers, HttpCode, HttpStatus, UseGuards } from "@nestjs/common";
|
|
import { ApiTags, ApiOperation, ApiParam, ApiResponse, ApiHeader } from "@nestjs/swagger";
|
|
import { ThrottlerGuard } from "@nestjs/throttler";
|
|
import { Logger } from "nestjs-pino";
|
|
import { Public } from "../../auth/decorators/public.decorator";
|
|
import { EnhancedWebhookSignatureGuard } from "../../webhooks/guards/enhanced-webhook-signature.guard";
|
|
import { OrderFulfillmentService } from "../services/order-fulfillment.service";
|
|
import type { OrderFulfillmentRequest } from "../services/order-fulfillment.service";
|
|
|
|
@ApiTags("order-fulfillment")
|
|
@Controller("orders")
|
|
@Public() // Salesforce webhook uses signature-based auth, not JWT
|
|
export class OrderFulfillmentController {
|
|
constructor(
|
|
private readonly orderFulfillmentService: OrderFulfillmentService,
|
|
private readonly logger: Logger
|
|
) {}
|
|
|
|
@Post(":sfOrderId/fulfill")
|
|
@HttpCode(HttpStatus.OK)
|
|
@UseGuards(ThrottlerGuard, EnhancedWebhookSignatureGuard)
|
|
@ApiOperation({
|
|
summary: "Fulfill order from Salesforce",
|
|
description: "Secure endpoint called by Salesforce Quick Action to fulfill orders in WHMCS. Handles complete flow: SF Order → WHMCS AddOrder/AcceptOrder → SF Status Update"
|
|
})
|
|
@ApiParam({
|
|
name: "sfOrderId",
|
|
type: String,
|
|
description: "Salesforce Order ID to provision",
|
|
example: "8014x000000ABCDXYZ"
|
|
})
|
|
@ApiHeader({
|
|
name: "X-SF-Signature",
|
|
description: "HMAC-SHA256 signature of request body using shared secret",
|
|
required: true,
|
|
example: "a1b2c3d4e5f6..."
|
|
})
|
|
@ApiHeader({
|
|
name: "X-SF-Timestamp",
|
|
description: "ISO timestamp of request (max 5 minutes old)",
|
|
required: true,
|
|
example: "2024-01-15T10:30:00Z"
|
|
})
|
|
@ApiHeader({
|
|
name: "X-SF-Nonce",
|
|
description: "Unique nonce to prevent replay attacks",
|
|
required: true,
|
|
example: "abc123def456"
|
|
})
|
|
@ApiHeader({
|
|
name: "Idempotency-Key",
|
|
description: "Unique key for safe retries",
|
|
required: true,
|
|
example: "provision_8014x000000ABCDXYZ_1705312200000"
|
|
})
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: "Order provisioning completed successfully",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
success: { type: "boolean", example: true },
|
|
status: { type: "string", enum: ["Provisioned", "Already Provisioned"], example: "Provisioned" },
|
|
whmcsOrderId: { type: "string", example: "12345" },
|
|
whmcsServiceIds: { type: "array", items: { type: "number" }, example: [67890, 67891] },
|
|
message: { type: "string", example: "Order provisioned successfully in WHMCS" }
|
|
}
|
|
}
|
|
})
|
|
@ApiResponse({
|
|
status: 400,
|
|
description: "Invalid request or order not found",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
success: { type: "boolean", example: false },
|
|
status: { type: "string", example: "Failed" },
|
|
message: { type: "string", example: "Salesforce order not found" },
|
|
errorCode: { type: "string", example: "ORDER_NOT_FOUND" }
|
|
}
|
|
}
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: "Invalid signature or authentication"
|
|
})
|
|
@ApiResponse({
|
|
status: 409,
|
|
description: "Payment method missing or other conflict",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
success: { type: "boolean", example: false },
|
|
status: { type: "string", example: "Failed" },
|
|
message: { type: "string", example: "Payment method missing - client must add payment method before provisioning" },
|
|
errorCode: { type: "string", example: "PAYMENT_METHOD_MISSING" }
|
|
}
|
|
}
|
|
})
|
|
async fulfillOrder(
|
|
@Param("sfOrderId") sfOrderId: string,
|
|
@Body() payload: OrderFulfillmentRequest,
|
|
@Headers("idempotency-key") idempotencyKey: string
|
|
) {
|
|
this.logger.log("Salesforce order fulfillment request received", {
|
|
sfOrderId,
|
|
idempotencyKey,
|
|
timestamp: payload.timestamp,
|
|
hasNonce: Boolean(payload.nonce),
|
|
});
|
|
|
|
try {
|
|
const result = await this.orderFulfillmentService.fulfillOrder(
|
|
sfOrderId,
|
|
payload,
|
|
idempotencyKey
|
|
);
|
|
|
|
this.logger.log("Salesforce provisioning completed", {
|
|
sfOrderId,
|
|
success: result.success,
|
|
status: result.status,
|
|
whmcsOrderId: result.whmcsOrderId,
|
|
serviceCount: result.whmcsServiceIds?.length || 0,
|
|
});
|
|
|
|
return {
|
|
success: result.success,
|
|
status: result.status,
|
|
whmcsOrderId: result.whmcsOrderId,
|
|
whmcsServiceIds: result.whmcsServiceIds,
|
|
message: result.message,
|
|
...(result.errorCode && { errorCode: result.errorCode }),
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
} catch (error) {
|
|
this.logger.error("Salesforce provisioning failed", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
sfOrderId,
|
|
idempotencyKey,
|
|
});
|
|
|
|
// Re-throw to let global exception handler format the response
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Removed /provision alias to avoid confusion — use /fulfill only
|
|
}
|