diff --git a/README.md b/README.md index 989d26c..5c658c7 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ If no target org is found, deploy exits with setup guidance. ## Configure Salesforce -After deploy, open **Setup** and configure the **Vector Map Geocoding** custom setting. This is a **hierarchy** custom setting: use **Manage** to set **organization** defaults so every user gets the same vector tile URL, geocoding provider, and (if needed) NSW Point API key. +After deploy, open **Setup** and configure the **Vector Map Geocoding** custom setting. This is a **hierarchy** custom setting: use **Manage** to set **organization** defaults so every user gets the same vector tile URL and geocoding provider. **Where to find it:** **Setup** → **Custom Settings** → **Vector Map Geocoding** → **Manage** (set values at the **Organization** level unless you need per-profile overrides). @@ -69,12 +69,19 @@ After deploy, open **Setup** and configure the **Vector Map Geocoding** custom s On the manage screen, create or edit the **default organization level** row. Set: -- **Geocoding Provider** — `OpenStreetMap` (Nominatim, default), `NSW_Point` (NSW Point geocoding; requires **NSW Point API Key**), or `None` (no external HTTP geocoding; use Salesforce compound Address lat/lng only). -- **NSW Point API Key** — required only when **Geocoding Provider** is NSW Point; used by Apex as the `x-api-key` header for NSW Point Geocode Address. +- **Geocoding Provider** — `OpenStreetMap` (Nominatim, default), `NSW_Point` (NSW Point geocoding; requires **NSW Point Named Credential** setup below), or `None` (no external HTTP geocoding; use Salesforce compound Address lat/lng only). - **Vector Tile Service URL** — optional ArcGIS **VectorTileServer** URL (must end with `VectorTileServer`). Leave blank to use the built-in NSW Spatial Services basemap URL (same default as Apex and the LWC). ![Manage Vector Map Geocoding — organization defaults](docs/images/CustomSettingsEdit.png) +### NSW Point API key (Named Credential) + +When **Geocoding Provider** is `NSW_Point`, store the API key in the **NSW Point Geocode** Named Credential (not in Custom Settings): + +1. **Setup** → **Named Credentials** → **NSW Point Geocode** → **External Credentials** tab → principal **NSW Point API Key**. +2. Set the **ApiKey** authentication parameter to your NSW Point API key (used as the `x-api-key` header on callouts). +3. Assign the **Vector Map NSW Point Geocode** permission set to users who need NSW Point geocoding (grants access to the External Credential principal). + The install script should setup remote sites to allow Salesforce to reach these services, but for reference they should look like this: ![Remote Site Settings](docs/images/RemoteSettings.png) diff --git a/force-app/main/default/classes/VectorMapGeocodeController.cls b/force-app/main/default/classes/VectorMapGeocodeController.cls index 5074fd5..f2d892b 100644 --- a/force-app/main/default/classes/VectorMapGeocodeController.cls +++ b/force-app/main/default/classes/VectorMapGeocodeController.cls @@ -11,8 +11,8 @@ public with sharing class VectorMapGeocodeController { private static final String SETTING_PROVIDER_NSW = 'NSW_Point'; /** Custom Setting: no HTTP callouts; compound Address lat/lng only. */ private static final String SETTING_PROVIDER_NONE = 'None'; - private static final String NSW_POINT_GEOCODE = - 'https://point.digital.nsw.gov.au/v3/api/geocodeAddress'; + /** Named Credential for NSW Point Geocode Address (x-api-key stored in External Credential). */ + private static final String NSW_POINT_NAMED_CREDENTIAL = 'NSW_Point_Geocode'; private static final String NOMINATIM_SEARCH = 'https://nominatim.openstreetmap.org/search'; private static final String NOMINATIM_USER_AGENT = @@ -75,13 +75,7 @@ public with sharing class VectorMapGeocodeController { return out; } if (SETTING_PROVIDER_NSW.equals(providerSetting)) { - String nswKey = getNswPointApiKey(); - if (String.isBlank(nswKey)) { - out.errorMessage = - 'Geocoding Provider is NSW Point but NSW Point API Key is not set in Custom Settings.'; - return out; - } - return tryNswPoint(trimmed, nswKey); + return tryNswPoint(trimmed); } return tryNominatim(trimmed); @@ -112,17 +106,9 @@ public with sharing class VectorMapGeocodeController { return SETTING_PROVIDER_OSM; } - private static String getNswPointApiKey() { - Vector_Map_Geocoding__c cfg = Vector_Map_Geocoding__c.getOrgDefaults(); - if (cfg == null || String.isBlank(cfg.NSW_Point_API_Key__c)) { - return null; - } - return cfg.NSW_Point_API_Key__c.trim(); - } - - private static GeocodeResult tryNswPoint(String address, String apiKey) { + private static GeocodeResult tryNswPoint(String address) { GeocodeResult out = new GeocodeResult(); - HttpRequest req = buildNswPointGeocodeRequest(address, apiKey); + HttpRequest req = buildNswPointGeocodeRequest(address); HttpResponse res = sendHttpRequest(req); if (res == null) { out.errorMessage = 'NSW Point request failed.'; @@ -181,19 +167,19 @@ public with sharing class VectorMapGeocodeController { } } - /** NSW Point Geocode Address: GET with x-api-key header. */ - private static HttpRequest buildNswPointGeocodeRequest( - String address, - String apiKey - ) { + /** NSW Point Geocode Address: GET via Named Credential (x-api-key from External Credential). */ + private static HttpRequest buildNswPointGeocodeRequest(String address) { String encodedAddress = EncodingUtil.urlEncode(address, 'UTF-8'); - String endpoint = NSW_POINT_GEOCODE + '?address=' + encodedAddress; + String endpoint = + 'callout:' + + NSW_POINT_NAMED_CREDENTIAL + + '/v3/api/geocodeAddress?address=' + + encodedAddress; HttpRequest req = new HttpRequest(); req.setEndpoint(endpoint); req.setMethod('GET'); req.setHeader('Accept', 'application/json'); req.setHeader('Accept-Crs', ''); - req.setHeader('x-api-key', apiKey); req.setTimeout(HTTP_TIMEOUT_MS); return req; } diff --git a/force-app/main/default/classes/VectorMapGeocodeControllerTest.cls b/force-app/main/default/classes/VectorMapGeocodeControllerTest.cls index 14344aa..d09ff38 100644 --- a/force-app/main/default/classes/VectorMapGeocodeControllerTest.cls +++ b/force-app/main/default/classes/VectorMapGeocodeControllerTest.cls @@ -11,10 +11,6 @@ private class VectorMapGeocodeControllerTest { !req.getEndpoint().contains('subscription-key='), 'NSW Point auth uses x-api-key header, not subscription-key query param' ); - System.assertEquals( - 'unit-test-nsw-key', - req.getHeader('x-api-key') - ); res.setStatusCode(200); res.setBody('{"latitude":-33.8688,"longitude":151.2093}'); return res; @@ -58,6 +54,15 @@ private class VectorMapGeocodeControllerTest { } } + private class NswHttp401Mock implements HttpCalloutMock { + public HttpResponse respond(HttpRequest req) { + HttpResponse res = new HttpResponse(); + res.setStatusCode(401); + res.setBody('{"error":"Unauthorized"}'); + return res; + } + } + @IsTest static void geocodeAddress_blank_returnsError() { Test.startTest(); @@ -71,7 +76,7 @@ private class VectorMapGeocodeControllerTest { @IsTest static void geocodeAddress_nswPoint_success() { - insertSettings('unit-test-nsw-key', 'NSW_Point'); + insertSettings('NSW_Point'); Test.setMock(HttpCalloutMock.class, new NswPointSuccessMock()); Test.startTest(); VectorMapGeocodeController.GeocodeResult r = VectorMapGeocodeController.geocodeAddress( @@ -86,7 +91,7 @@ private class VectorMapGeocodeControllerTest { @IsTest static void geocodeAddress_nswPoint_parsesEsriCandidatesJson() { - insertSettings('key-esri', 'NSW_Point'); + insertSettings('NSW_Point'); Test.setMock(HttpCalloutMock.class, new NswPointEsriCandidatesMock()); Test.startTest(); VectorMapGeocodeController.GeocodeResult r = VectorMapGeocodeController.geocodeAddress( @@ -140,7 +145,7 @@ private class VectorMapGeocodeControllerTest { @IsTest static void geocodeAddress_nswPoint_httpError_returnsError() { - insertSettings('key-for-test', 'NSW_Point'); + insertSettings('NSW_Point'); Test.setMock(HttpCalloutMock.class, new NswHttp500Mock()); Test.startTest(); VectorMapGeocodeController.GeocodeResult r = VectorMapGeocodeController.geocodeAddress( @@ -153,12 +158,9 @@ private class VectorMapGeocodeControllerTest { } @IsTest - static void geocodeAddress_nswPoint_noApiKey_returnsError() { - insert new Vector_Map_Geocoding__c( - SetupOwnerId = UserInfo.getOrganizationId(), - Geocoding_Provider__c = 'NSW_Point', - NSW_Point_API_Key__c = null - ); + static void geocodeAddress_nswPoint_unauthorized_returnsError() { + insertSettings('NSW_Point'); + Test.setMock(HttpCalloutMock.class, new NswHttp401Mock()); Test.startTest(); VectorMapGeocodeController.GeocodeResult r = VectorMapGeocodeController.geocodeAddress( 'Sydney' @@ -166,15 +168,16 @@ private class VectorMapGeocodeControllerTest { Test.stopTest(); System.assertNotEquals(null, r.errorMessage); System.assert( - r.errorMessage.contains('API Key'), - 'Expected message about API key: ' + r.errorMessage + r.errorMessage.contains('401'), + 'Expected HTTP 401 in error: ' + r.errorMessage ); + System.assertEquals(null, r.latitude); + System.assertEquals(null, r.longitude); } - private static void insertSettings(String apiKey, String geocodingProvider) { + private static void insertSettings(String geocodingProvider) { insert new Vector_Map_Geocoding__c( SetupOwnerId = UserInfo.getOrganizationId(), - NSW_Point_API_Key__c = apiKey, Geocoding_Provider__c = geocodingProvider ); } diff --git a/force-app/main/default/externalCredentials/NSW_Point_Geocode.externalCredential-meta.xml b/force-app/main/default/externalCredentials/NSW_Point_Geocode.externalCredential-meta.xml new file mode 100644 index 0000000..c1d8f12 --- /dev/null +++ b/force-app/main/default/externalCredentials/NSW_Point_Geocode.externalCredential-meta.xml @@ -0,0 +1,24 @@ + + + Custom + + NSW_Point_API_Key + NSW_Point_API_Key + NamedPrincipal + 1 + + + NSW_Point_API_Key + ApiKey + AuthParameter + 2 + + + DefaultGroup + x-api-key + AuthHeader + {!$Credential.NSW_Point_Geocode.ApiKey} + 1 + + + diff --git a/force-app/main/default/lwc/vectorMap/vectorMap.html b/force-app/main/default/lwc/vectorMap/vectorMap.html index 26b80e0..eaaab05 100644 --- a/force-app/main/default/lwc/vectorMap/vectorMap.html +++ b/force-app/main/default/lwc/vectorMap/vectorMap.html @@ -85,7 +85,7 @@

Addres diff --git a/force-app/main/default/lwc/vectorMap/vectorMap.js-meta.xml b/force-app/main/default/lwc/vectorMap/vectorMap.js-meta.xml index 83c813e..5b3456a 100644 --- a/force-app/main/default/lwc/vectorMap/vectorMap.js-meta.xml +++ b/force-app/main/default/lwc/vectorMap/vectorMap.js-meta.xml @@ -42,7 +42,7 @@ type="Boolean" label="External geocoding" default="true" - description="When true (recommended), geocode from the displayed address text via Apex; coordinates override Salesforce compound lat/lng. Provider and NSW Point API key: Setup — Custom Settings — Vector Map Geocoding. Turn off to use only Salesforce Address coordinates. Respect Nominatim usage policy when using OpenStreetMap." + description="When true (recommended), geocode from the displayed address text via Apex; coordinates override Salesforce compound lat/lng. Provider: Setup — Custom Settings — Vector Map Geocoding. NSW Point API key: Setup — Named Credentials — NSW Point Geocode. Turn off to use only Salesforce Address coordinates. Respect Nominatim usage policy when using OpenStreetMap." /> + + false + false + Enabled + false + + + Url + Url + https://point.digital.nsw.gov.au + + + NSW_Point_Geocode + ExternalCredential + Authentication + + SecuredEndpoint + diff --git a/force-app/main/default/objects/Vector_Map_Geocoding__c/Vector_Map_Geocoding__c.object-meta.xml b/force-app/main/default/objects/Vector_Map_Geocoding__c/Vector_Map_Geocoding__c.object-meta.xml index 445e24a..1c5183f 100644 --- a/force-app/main/default/objects/Vector_Map_Geocoding__c/Vector_Map_Geocoding__c.object-meta.xml +++ b/force-app/main/default/objects/Vector_Map_Geocoding__c/Vector_Map_Geocoding__c.object-meta.xml @@ -1,8 +1,8 @@ Hierarchy - Org-level settings for Vector Map: vector tile URL, external geocoding provider, and NSW Point API key. Used by Vector Map and Spatial Services Map. + Org-level settings for Vector Map: vector tile URL and external geocoding provider. NSW Point API key is stored in the NSW Point Geocode Named Credential. false - Public + Protected diff --git a/force-app/main/default/objects/Vector_Map_Geocoding__c/fields/Geocoding_Provider__c.field-meta.xml b/force-app/main/default/objects/Vector_Map_Geocoding__c/fields/Geocoding_Provider__c.field-meta.xml index b597994..301f71f 100644 --- a/force-app/main/default/objects/Vector_Map_Geocoding__c/fields/Geocoding_Provider__c.field-meta.xml +++ b/force-app/main/default/objects/Vector_Map_Geocoding__c/fields/Geocoding_Provider__c.field-meta.xml @@ -1,7 +1,7 @@ Geocoding_Provider__c - Address lookup service: OpenStreetMap (Nominatim, default), NSW_Point (requires NSW Point API Key), or None (Salesforce Address latitude/longitude only; no external lookup). Blank defaults to OpenStreetMap. + Address lookup service: OpenStreetMap (Nominatim, default), NSW_Point (requires NSW Point Geocode Named Credential), or None (Salesforce Address latitude/longitude only; no external lookup). Blank defaults to OpenStreetMap. false 32 diff --git a/force-app/main/default/objects/Vector_Map_Geocoding__c/fields/NSW_Point_API_Key__c.field-meta.xml b/force-app/main/default/objects/Vector_Map_Geocoding__c/fields/NSW_Point_API_Key__c.field-meta.xml deleted file mode 100644 index c0749ee..0000000 --- a/force-app/main/default/objects/Vector_Map_Geocoding__c/fields/NSW_Point_API_Key__c.field-meta.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - NSW_Point_API_Key__c - API key for NSW Point Geocode Address (v3). Set under Setup — Custom Settings — Vector Map Geocoding — Manage (organization). - false - - 255 - false - false - Text - false - diff --git a/force-app/main/default/permissionsets/Vector_Map_NSW_Point_Geocode.permissionset-meta.xml b/force-app/main/default/permissionsets/Vector_Map_NSW_Point_Geocode.permissionset-meta.xml new file mode 100644 index 0000000..db0bce4 --- /dev/null +++ b/force-app/main/default/permissionsets/Vector_Map_NSW_Point_Geocode.permissionset-meta.xml @@ -0,0 +1,10 @@ + + + Grants access to the NSW Point Geocode Named Credential used by Vector Map external geocoding. + + true + NSW_Point_Geocode-NSW_Point_API_Key + + false + + diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 1698bcb..0816f1c 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -99,19 +99,22 @@ deploy_phase() { sf project deploy start "${COMMON_ARGS[@]}" "$@" } -# 1) Foundation metadata: endpoint allowlists and custom object schema. -deploy_phase "Phase 1/3: Remote Site Settings + Custom Object schema" \ +# 1) Foundation metadata: endpoint allowlists, credentials, and custom object schema. +deploy_phase "Phase 1/4: Remote Site Settings + Credentials + Custom Object schema" \ --source-dir force-app/main/default/remoteSiteSettings \ + --source-dir force-app/main/default/externalCredentials \ + --source-dir force-app/main/default/namedCredentials \ + --source-dir force-app/main/default/permissionsets \ --source-dir force-app/main/default/objects # 2) Server-side logic + VF wrapper pages. -deploy_phase "Phase 2/3: Apex Classes + Visualforce Pages" \ +deploy_phase "Phase 2/4: Apex Classes + Visualforce Pages" \ --source-dir force-app/main/default/classes \ --source-dir force-app/main/default/pages \ --test-level "$TEST_LEVEL" # 3) Front-end assets and LWC. -deploy_phase "Phase 3/3: Static Resources + LWC" \ +deploy_phase "Phase 3/4: Static Resources + LWC" \ --source-dir force-app/main/default/staticresources \ --source-dir force-app/main/default/lwc