- Simplified import statements in auth.controller.ts and consolidated DTO imports. - Streamlined accountStatus method in AuthController for better clarity. - Refactored error handling in AuthService for existing mapping checks and password validation. - Cleaned up whitespace and formatting across various files for consistency. - Enhanced logging configuration in logging.module.ts to reduce noise and improve clarity. - Updated frontend components for better formatting and readability in ProfilePage and SignupPage.
9.6 KiB
9.6 KiB
Simple Salesforce-to-Portal Communication Guide
The Simple Flow (No Reverse Webhooks Needed!)
1. Customer places order → Portal creates Salesforce Order (Pending Review)
2. Salesforce operator → Clicks "Provision in WHMCS" Quick Action
3. Salesforce → Calls Portal BFF → POST /orders/{sfOrderId}/provision
4. Portal BFF → Provisions in WHMCS → DIRECTLY updates Salesforce Order (via existing SF API)
5. Customer → Polls Portal for status updates
Key insight: You already have Salesforce API access in your Portal BFF, so you can directly update the Order status. No reverse webhooks needed!
1. Salesforce Quick Action Security
Salesforce Apex (Secure Call to Portal)
public class OrderProvisioningService {
private static final String WEBHOOK_SECRET = '{!$Credential.Portal_Webhook.Password}';
@future(callout=true)
public static void provisionOrder(String orderId) {
try {
// Simple secure payload
Map<String, Object> payload = new Map<String, Object>{
'orderId' => orderId,
'timestamp' => System.now().format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''),
'nonce' => generateNonce()
};
String jsonPayload = JSON.serialize(payload);
String signature = generateHMACSignature(jsonPayload, WEBHOOK_SECRET);
// Call Portal BFF
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Portal_BFF/orders/' + orderId + '/provision');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setHeader('X-SF-Signature', signature);
req.setHeader('X-SF-Timestamp', payload.get('timestamp').toString());
req.setHeader('X-SF-Nonce', payload.get('nonce').toString());
req.setHeader('Idempotency-Key', 'provision_' + orderId + '_' + System.now().getTime());
req.setBody(jsonPayload);
req.setTimeout(30000);
Http http = new Http();
HttpResponse res = http.send(req);
// Simple response handling
if (res.getStatusCode() != 200) {
throw new Exception('Portal returned: ' + res.getStatusCode());
}
} catch (Exception e) {
// Update order status on failure
updateOrderStatus(orderId, 'Failed', e.getMessage());
}
}
private static String generateHMACSignature(String data, String key) {
Blob hmacData = Crypto.generateMac('HmacSHA256', Blob.valueOf(data), Blob.valueOf(key));
return EncodingUtil.convertToHex(hmacData);
}
private static String generateNonce() {
return EncodingUtil.convertToHex(Crypto.generateAesKey(128)).substring(0, 16);
}
private static void updateOrderStatus(String orderId, String status, String errorMessage) {
Order ord = [SELECT Id FROM Order WHERE Id = :orderId LIMIT 1];
ord.Provisioning_Status__c = status;
if (errorMessage != null) {
ord.Provisioning_Error_Message__c = errorMessage.left(255);
}
update ord;
}
}
2. Portal BFF Implementation (Simple!)
Enhanced Security for Provisioning Endpoint
// apps/bff/src/orders/orders.controller.ts
@Post(':sfOrderId/provision')
@UseGuards(EnhancedWebhookSignatureGuard) // Your existing guard
@ApiOperation({ summary: "Provision order from Salesforce" })
async provisionOrder(
@Param('sfOrderId') sfOrderId: string,
@Body() payload: { orderId: string; timestamp: string; nonce: string },
@Headers('idempotency-key') idempotencyKey: string
) {
return await this.orderOrchestrator.provisionOrder(sfOrderId, payload, idempotencyKey);
}
Order Orchestrator (Direct Salesforce Updates)
// apps/bff/src/orders/services/order-orchestrator.service.ts
@Injectable()
export class OrderOrchestrator {
constructor(
private salesforceService: SalesforceService, // Your existing service
private whmcsService: WhmcsService,
@Inject(Logger) private logger: Logger
) {}
async provisionOrder(sfOrderId: string, payload: any, idempotencyKey: string) {
try {
// 1. Update SF status to "Activating"
await this.updateSalesforceOrderStatus(sfOrderId, 'Activating');
// 2. Your existing provisioning logic
const result = await this.provisionInWhmcs(sfOrderId);
// 3. Update SF status to "Provisioned" with WHMCS ID
await this.updateSalesforceOrderStatus(sfOrderId, 'Provisioned', {
whmcsOrderId: result.whmcsOrderId,
});
this.logger.log('Order provisioned successfully', {
sfOrderId,
whmcsOrderId: result.whmcsOrderId,
});
return {
success: true,
status: 'Provisioned',
whmcsOrderId: result.whmcsOrderId,
};
} catch (error) {
// Update SF status to "Failed"
await this.updateSalesforceOrderStatus(sfOrderId, 'Failed', {
errorCode: 'PROVISIONING_ERROR',
errorMessage: error instanceof Error ? error.message : String(error),
});
this.logger.error('Order provisioning failed', {
sfOrderId,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
// Simple direct Salesforce update (using your existing SF service)
private async updateSalesforceOrderStatus(
sfOrderId: string,
status: 'Activating' | 'Provisioned' | 'Failed',
details?: {
whmcsOrderId?: string;
errorCode?: string;
errorMessage?: string;
}
) {
try {
const updateData: any = {
Id: sfOrderId,
Provisioning_Status__c: status,
Last_Provisioning_At__c: new Date().toISOString(),
};
if (details?.whmcsOrderId) {
updateData.WHMCS_Order_ID__c = details.whmcsOrderId;
}
if (status === 'Failed' && details?.errorCode) {
updateData.Provisioning_Error_Code__c = details.errorCode;
updateData.Provisioning_Error_Message__c = details.errorMessage?.substring(0, 255);
}
// Use your existing Salesforce service to update
await this.salesforceService.updateOrder(updateData);
this.logger.log('Salesforce order status updated', {
sfOrderId,
status,
});
} catch (error) {
this.logger.error('Failed to update Salesforce order status', {
sfOrderId,
status,
error: error instanceof Error ? error.message : String(error),
});
// Don't throw - provisioning succeeded, this is just a status update
}
}
}
Add Update Method to Salesforce Service
// apps/bff/src/vendors/salesforce/salesforce.service.ts
// Add this method to your existing SalesforceService
async updateOrder(orderData: { Id: string; [key: string]: any }): Promise<void> {
try {
const sobject = this.connection.sobject('Order');
await sobject.update(orderData);
this.logger.log('Order updated in Salesforce', {
orderId: orderData.Id,
fields: Object.keys(orderData).filter(k => k !== 'Id'),
});
} catch (error) {
this.logger.error('Failed to update order in Salesforce', {
orderId: orderData.Id,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
3. Customer UI (Simple Polling)
// Portal UI - simple polling for order status
export function useOrderStatus(sfOrderId: string) {
const [orderStatus, setOrderStatus] = useState<{
status: string;
whmcsOrderId?: string;
error?: string;
}>({ status: 'Pending Review' });
useEffect(() => {
const pollStatus = async () => {
try {
const response = await fetch(`/api/orders/${sfOrderId}`);
const data = await response.json();
setOrderStatus(data);
// Stop polling when complete
if (['Provisioned', 'Failed'].includes(data.status)) {
clearInterval(interval);
}
} catch (error) {
console.error('Failed to fetch order status:', error);
}
};
const interval = setInterval(pollStatus, 5000); // Poll every 5 seconds
pollStatus(); // Initial fetch
return () => clearInterval(interval);
}, [sfOrderId]);
return orderStatus;
}
4. Security Configuration
Environment Variables (Simple)
# Salesforce webhook security
SF_WEBHOOK_SECRET=your_256_bit_secret_key_here
SF_WEBHOOK_IP_ALLOWLIST=13.108.0.0/14,204.14.232.0/23
WEBHOOK_TIMESTAMP_TOLERANCE=300000 # 5 minutes
Salesforce Named Credentials
<!-- For API calls -->
<NamedCredential>
<fullName>Portal_BFF</fullName>
<endpoint>https://your-portal-api.com</endpoint>
<principalType>Anonymous</principalType>
<protocol>HttpsOnly</protocol>
</NamedCredential>
<!-- For webhook secret -->
<NamedCredential>
<fullName>Portal_Webhook</fullName>
<endpoint>https://your-portal-api.com</endpoint>
<principalType>NamedPrincipal</principalType>
<password>your_256_bit_secret_key_here</password>
<username>webhook</username>
</NamedCredential>
Summary: Why This is Simple
✅ No reverse webhooks - Portal directly updates Salesforce via existing API
✅ One-way communication - Salesforce → Portal → Direct SF update
✅ Uses existing infrastructure - Your SF service, webhook guards, etc.
✅ Simple customer experience - Portal polls for status updates
✅ Production ready - HMAC security, idempotency, error handling
This follows exactly what your docs specify: Salesforce calls Portal, Portal provisions and updates Salesforce directly. Much cleaner!