123 lines
7.2 KiB
OpenEdge ABL
123 lines
7.2 KiB
OpenEdge ABL
/**
|
|
* ─────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
* Removes the org's self-callout for the webhook hot path.
|
|
*
|
|
* Previously, every Shopify webhook made the org POST an authenticated HTTP request to ITS OWN Apex
|
|
* REST endpoints (SfUtility.postToSF), carrying a Bearer access token obtained from the stored refresh
|
|
* token. That is the entire reason ordinary webhook traffic touched the refresh token at all — and under
|
|
* the Connected App's mandatory Refresh Token Rotation, every one of those touches is a chance to lose a
|
|
* rotation (a dropped connection between Salesforce minting the replacement and this org receiving it is
|
|
* unavoidable over any network, however rare). Removing the network hop removes that exposure entirely
|
|
* for every object type wired up below: no HTTP call, no Bearer token, no rotation, nothing to lose.
|
|
*
|
|
* HOW: each handler below is an existing @RestResource class whose method reads its input straight from
|
|
* RestContext.request — nothing about their internals changes. This fabricates a RestRequest/RestResponse
|
|
* carrying the same body/params an HTTP call would have delivered, swaps it into RestContext for the
|
|
* duration of the call, and restores the ORIGINAL context before returning — the outer transaction (the
|
|
* real Shopify webhook hit, itself running inside a REST context) must see its own RestContext
|
|
* unaffected once this returns. This swap-and-restore is not a new trick: this codebase's own test suite
|
|
* already does exactly this (see UpdateRefreshToken_Test.cls) to invoke a REST method without a real
|
|
* HTTP call.
|
|
*
|
|
* SAFE BY CONSTRUCTION: only urlMapping suffixes explicitly listed in DIRECTLY_DISPATCHABLE are handled
|
|
* here. Anything else — a mapping added later, a typo, an object type this class doesn't yet know about —
|
|
* returns false, and SfUtility.postToSF falls back to the original HTTP relay unchanged. Nothing that
|
|
* isn't explicitly wired up here can regress.
|
|
* ──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
*/
|
|
public with sharing class DirectWebhookDispatcher {
|
|
|
|
private static final Set<String> DIRECTLY_DISPATCHABLE = new Set<String>{
|
|
'UpdateCustomerFromShopify', 'DeleteCustomerFromShopify',
|
|
'UpdateProductAndVarientFromShopify', 'DeleteProductAndVariantfromShopify',
|
|
'UpdateOrderFromShopify', 'DeleteOrderFromShopify',
|
|
'UpdateCollectionAndCollectsFromShopify', 'DeleteCollectionFromShopify',
|
|
'UpdateInventoryLevelsFromShopify', 'DisconnectInventoryFormShopify',
|
|
'UpdateDraftOrderFromShopify', 'UpdateLocationFromShopify', 'DeleteLocationFromShopify'
|
|
};
|
|
|
|
/**
|
|
* @return true — the mapping was recognised and handled here; the caller must NOT also relay over
|
|
* HTTP (that would process the same webhook twice).
|
|
* false — not a mapping this class knows about; the caller should fall back to the HTTP relay.
|
|
*/
|
|
public static Boolean dispatch(String urlMapping, String body, String store, String objectName) {
|
|
if (String.isBlank(urlMapping)) return false;
|
|
String handlerName = urlMapping.contains('/') ? urlMapping.substringAfterLast('/') : urlMapping;
|
|
if (!DIRECTLY_DISPATCHABLE.contains(handlerName)) return false;
|
|
|
|
RestRequest savedReq = RestContext.request;
|
|
RestResponse savedRes = RestContext.response;
|
|
try {
|
|
RestRequest innerReq = new RestRequest();
|
|
innerReq.requestBody = Blob.valueOf(body == null ? '' : body);
|
|
innerReq.httpMethod = 'POST';
|
|
// RestRequest.params is a final property — already initialized to an empty map by the
|
|
// constructor, so it must be populated via put(), not replaced with a new map.
|
|
innerReq.params.put('store', store);
|
|
innerReq.params.put('objectname', objectName);
|
|
RestContext.request = innerReq;
|
|
RestContext.response = new RestResponse();
|
|
|
|
switch on handlerName {
|
|
when 'UpdateCustomerFromShopify' {
|
|
UpdateCustomerFromShopify.updateCustomer();
|
|
}
|
|
when 'DeleteCustomerFromShopify' {
|
|
DeleteCustomerFromShopify.DeleteCustomer();
|
|
}
|
|
when 'UpdateProductAndVarientFromShopify' {
|
|
UpdateProductAndVarientFromShopify.UpdateProductandVarients();
|
|
}
|
|
when 'DeleteProductAndVariantfromShopify' {
|
|
DeleteProductAndVariantfromShopify.DeleteProduct();
|
|
}
|
|
when 'UpdateOrderFromShopify' {
|
|
UpdateOrderFromShopify.updateOrder();
|
|
}
|
|
when 'DeleteOrderFromShopify' {
|
|
DeleteOrderFromShopify.DeleteOrder();
|
|
}
|
|
when 'UpdateCollectionAndCollectsFromShopify' {
|
|
UpdateCollectionAndCollectsFromShopify.updateCollectionAndCollects();
|
|
}
|
|
when 'DeleteCollectionFromShopify' {
|
|
DeleteCollectionFromShopify.deleteCollection();
|
|
}
|
|
when 'UpdateInventoryLevelsFromShopify' {
|
|
UpdateInventoryLevelsFromShopify.UpdateIventory();
|
|
}
|
|
when 'DisconnectInventoryFormShopify' {
|
|
DisconnectInventoryFormShopify.DeleteVariant();
|
|
}
|
|
when 'UpdateDraftOrderFromShopify' {
|
|
UpdateDraftOrderFromShopify.updateOrder();
|
|
}
|
|
when 'UpdateLocationFromShopify' {
|
|
UpdateLocationFromShopify.updatelocation();
|
|
}
|
|
when 'DeleteLocationFromShopify' {
|
|
DeleteLocationFromShopify.DeleteLocation();
|
|
}
|
|
when else {
|
|
return false; // unreachable given the Set check above; safe fallback regardless
|
|
}
|
|
}
|
|
return true;
|
|
|
|
} catch (Exception e) {
|
|
// The handler threw. Do not let that vanish: ask Shopify to redeliver rather than let the
|
|
// caller believe the webhook succeeded. Set the status on the SAVED (outer, real) response —
|
|
// not on RestContext.response, which at this point still refers to the inner one and is
|
|
// about to be discarded.
|
|
System.debug(LoggingLevel.ERROR, 'SME_DISPATCH direct dispatch failed for ' + handlerName
|
|
+ ' — ' + e.getTypeName() + ' | ' + e.getMessage() + ' | line ' + e.getLineNumber());
|
|
if (savedRes != null) savedRes.statusCode = 503;
|
|
return true; // handled (as a failure) — the caller must not ALSO attempt the HTTP relay
|
|
|
|
} finally {
|
|
RestContext.request = savedReq;
|
|
RestContext.response = savedRes;
|
|
}
|
|
}
|
|
} |