From b52e6a6becc6cc3bf87ea7e8bb4caa26eb1686b6 Mon Sep 17 00:00:00 2001 From: Rick Saccoccia Date: Wed, 25 Sep 2024 11:24:09 -0600 Subject: [PATCH] arcgis username and password authentication --- plugins/arcgis/service/package-lock.json | 191 +++++++++++++++++- plugins/arcgis/service/package.json | 2 + plugins/arcgis/service/src/ArcGISConfig.ts | 21 ++ plugins/arcgis/service/src/index.ts | 115 +++++++++-- .../projects/main/src/lib/ArcGISConfig.ts | 21 ++ .../src/lib/arc-layer/arc-layer.component.ts | 159 ++++++++++----- 6 files changed, 446 insertions(+), 63 deletions(-) diff --git a/plugins/arcgis/service/package-lock.json b/plugins/arcgis/service/package-lock.json index 8fc472a33..246d648d1 100644 --- a/plugins/arcgis/service/package-lock.json +++ b/plugins/arcgis/service/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { + "@esri/arcgis-rest-feature-service": "^4.0.6", + "@esri/arcgis-rest-request": "^4.2.3", "@terraformer/arcgis": "2.1.2", "form-data": "^4.0.0" }, @@ -635,6 +637,69 @@ "node": ">=0.1.95" } }, + "node_modules/@esri/arcgis-rest-feature-service": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-feature-service/-/arcgis-rest-feature-service-4.0.6.tgz", + "integrity": "sha512-M1iqrI5dq1PZOgIafqHvnkBtVpLWafAl7+w+TXVZ+P68FiDf44FGoX4cyEWi7E83ejFeIfSuM6sU61eI9K3hNg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@esri/arcgis-rest-portal": "^4.0.0", + "@esri/arcgis-rest-request": "^4.0.0" + } + }, + "node_modules/@esri/arcgis-rest-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-fetch/-/arcgis-rest-fetch-4.0.0.tgz", + "integrity": "sha512-ybsMO2L4cxx0IaIx0jv6/VbXidZmQIiGD3bvPF1/n1Y1ljHIhCvX+ti54cQSfg/HW2+VAVVnt8EPD/omVhNAyg==", + "dependencies": { + "node-fetch": "^3.0.0" + } + }, + "node_modules/@esri/arcgis-rest-form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-form-data/-/arcgis-rest-form-data-4.0.0.tgz", + "integrity": "sha512-cAS9HONIJgseCDdgRCIHBR4CE/OQXZIRP3FoNx/w+XyjVqK6yQiwCeXpYLfHeEZ6GEQqrA9XUBxifWG9UaJyXA==", + "dependencies": { + "formdata-node": "^4.1.0" + } + }, + "node_modules/@esri/arcgis-rest-portal": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-portal/-/arcgis-rest-portal-4.4.1.tgz", + "integrity": "sha512-RwOLxWDq+c0ew6N5iK6k3G7IgVH35Saqs79Z5OCiUYqRFHSlN5HCkAkXlqxX+04ByYksO/Tmg/RvbZEXpS8U9Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@esri/arcgis-rest-request": "^4.0.0" + } + }, + "node_modules/@esri/arcgis-rest-request": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-request/-/arcgis-rest-request-4.2.3.tgz", + "integrity": "sha512-5jpgZIN9v8L4q1HRFG1rvn10ay/8UBskvjFMfmvGX+0AsZVVo339ENW1VIwZUoYKv7s06FRWGOcO6EYHq+e+6A==", + "license": "Apache-2.0", + "dependencies": { + "@esri/arcgis-rest-fetch": "^4.0.0", + "@esri/arcgis-rest-form-data": "^4.0.0", + "mitt": "^3.0.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -7367,8 +7432,6 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "extraneous": true, - "optional": true, - "dev": true, "hasInstallScript": true, "os": [ "darwin" @@ -16024,6 +16087,15 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -16802,6 +16874,38 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-blob/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -16900,6 +17004,31 @@ "node": ">= 6" } }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -19433,6 +19562,12 @@ "node": ">=0.10.0" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -19550,6 +19685,43 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -21765,6 +21937,12 @@ "node": ">=10" } }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -22159,6 +22337,15 @@ "makeerror": "1.0.12" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", diff --git a/plugins/arcgis/service/package.json b/plugins/arcgis/service/package.json index e2139ba1d..c1653745a 100644 --- a/plugins/arcgis/service/package.json +++ b/plugins/arcgis/service/package.json @@ -38,6 +38,8 @@ }, "homepage": "https://github.com/ngageoint/mage-server#readme", "dependencies": { + "@esri/arcgis-rest-feature-service": "^4.0.6", + "@esri/arcgis-rest-request": "^4.2.3", "@terraformer/arcgis": "2.1.2", "form-data": "^4.0.0" }, diff --git a/plugins/arcgis/service/src/ArcGISConfig.ts b/plugins/arcgis/service/src/ArcGISConfig.ts index df860dfdc..be99ed10b 100644 --- a/plugins/arcgis/service/src/ArcGISConfig.ts +++ b/plugins/arcgis/service/src/ArcGISConfig.ts @@ -13,6 +13,11 @@ export interface FeatureServiceConfig { */ token?: string + /** + * Username and password for ArcGIS authentication + */ + auth?: ArcGISAuthConfig + /** * Create layers that don't exist */ @@ -72,6 +77,22 @@ export interface FeatureLayerConfig { } +/** + * Contains username and password for ArcGIS server authentication. + */ +export interface ArcGISAuthConfig { + + /** + * The username for authentication. + */ + username: string + + /** + * The password for authentication. + */ + password: string +} + /** * Attribute configurations */ diff --git a/plugins/arcgis/service/src/index.ts b/plugins/arcgis/service/src/index.ts index f888cabf3..ddfbedf06 100644 --- a/plugins/arcgis/service/src/index.ts +++ b/plugins/arcgis/service/src/index.ts @@ -9,6 +9,9 @@ import { ArcGISPluginConfig } from './ArcGISPluginConfig' import { ObservationProcessor } from './ObservationProcessor' import {HttpClient} from './HttpClient' import { FeatureServiceResult } from './FeatureServiceResult' +import { ArcGISIdentityManager } from "@esri/arcgis-rest-request" +// import { IQueryFeaturesOptions, queryFeatures } from '@esri/arcgis-rest-feature-service' + const logPrefix = '[mage.arcgis]' const logMethods = ['log', 'debug', 'info', 'warn', 'error'] as const @@ -32,6 +35,69 @@ const InjectedServices = { userRepo: UserRepositoryToken } +/** + * Provides the portal URL from a given feature URL. + * + * @param {string} featureServiceUrl - The URL of the feature service. + * @returns {string} The portal URL. + */ +function getPortalUrl(featureServiceUrl: string): string { + const url = new URL(featureServiceUrl); + return `https://${url.hostname}/arcgis/sharing/rest`; +} + +/** + * Provides the server URL from a given feature URL. + * + * @param {string} featureServiceUrl - The URL of the feature service. + * @returns {string} The server URL. + */ +function getServerUrl(featureServiceUrl: string): string { + const url = new URL(featureServiceUrl); + return `https://${url.hostname}/arcgis`; +} + + /** + * Handles authentication for a given request. + * + * @param {express.Request} req The express request object. + * @returns {Promise} The authenticated identity manager. + * + * @throws {Error} If the identity manager could not be created due to missing required query parameters. + */ +async function handleAuthentication(req: express.Request): Promise { + const featureUsername = req.query.username as string | undefined; + const featurePassword = req.query.password as string | undefined; + const featureServer = req.query.server as string | undefined; + const featurePortal = req.query.portal as string | undefined; + const featureToken = req.query.token as string | undefined; + + let identityManager: ArcGISIdentityManager; + + try { + if (featureToken) { + console.log('Token provided for authentication'); + identityManager = await ArcGISIdentityManager.fromToken({ token: featureToken, server: getServerUrl(req.query.featureUrl as string ?? ''), portal: getPortalUrl(req.query.featureUrl as string ?? '') }); + } else if (featureUsername && featurePassword) { + console.log('Username and password provided for authentication, username:' + featureUsername); + identityManager = await ArcGISIdentityManager.signIn({ + username: featureUsername, + password: featurePassword, + portal: getPortalUrl(req.query.featureUrl as string ?? ''), + }); + } else { + throw new Error('Missing required query parameters to authenticate (token or username/password).'); + } + + console.log('Identity Manager token', identityManager.token); + console.log('Identity Manager token expires', identityManager.tokenExpires); //TODO - is expiration arbitrary from ArcGISIdentityManager or actually the correct expiration allowed by server? Why undefined days later? + } catch (error) { + console.error('Error during authentication:', error); + throw new Error('Authentication failed.'); + } + return identityManager; +} + /** * The MAGE ArcGIS Plugin finds new MAGE observations and if configured to send the observations * to an ArcGIS server, it will then transform the observation to an ArcGIS feature and @@ -47,6 +113,12 @@ const arcgisPluginHooks: InitPluginHook = { init: async (services): Promise => { console.info('Intializing ArcGIS plugin...') const { stateRepo, eventRepo, obsRepoForEvent, userRepo } = services + // TODO + // - Move getServerUrl to Helper file + // - Move getPortalUrl to Helper file + // - Update layer token to get token from identity manager + // - Move plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts addLayer to helper file and use instead of encodeURIComponent + const processor = new ObservationProcessor(stateRepo, eventRepo, obsRepoForEvent, userRepo, console); processor.start(); return { @@ -64,7 +136,7 @@ const arcgisPluginHooks: InitPluginHook = { routes.route('/config') .get(async (req, res, next) => { console.info('Getting ArcGIS plugin config...') - const config = await processor.safeGetConfig(); + const config = await processor.safeGetConfig(); res.json(config) }) .put(async (req, res, next) => { @@ -73,27 +145,38 @@ const arcgisPluginHooks: InitPluginHook = { const configString = JSON.stringify(arcConfig) console.info(configString) processor.putConfig(arcConfig) - res.status(200).json({}) + res.status(200).json({}) // TODO: Why returning 200 with an empty object here, should we update? }) routes.route('/arcgisLayers') .get(async (req, res, next) => { const featureUrl = req.query.featureUrl as string; console.info('Getting ArcGIS layer info for ' + featureUrl) - const httpClient = new HttpClient(console); - httpClient.sendGetHandleResponse(featureUrl, (chunk) => { - console.info('ArcGIS layer info response ' + chunk); - try { - const featureServiceResult = JSON.parse(chunk) as FeatureServiceResult; - res.json(featureServiceResult); - } catch(e) { - if(e instanceof SyntaxError) { - console.error('Problem with url response for url ' + featureUrl + ' error ' + e) - res.status(200).json({}) - } else { - throw e; + let identityManager: ArcGISIdentityManager; + + try { + identityManager = await handleAuthentication(req); + + const featureUrlAndToken = featureUrl + '?token=' + encodeURIComponent(identityManager.token); + console.log('featureUrlAndToken', featureUrlAndToken); + const httpClient = new HttpClient(console); + + httpClient.sendGetHandleResponse(featureUrlAndToken, (chunk) => { + console.info('ArcGIS layer info response ' + chunk); + try { + const featureServiceResult = JSON.parse(chunk) as FeatureServiceResult; + res.json(featureServiceResult); + } catch (e) { + if (e instanceof SyntaxError) { + console.error('Problem with url response for url ' + featureUrl + ' error ' + e) + res.status(200).json({}) // TODO: Why returning 200 with an empty object here, should we update? + } else { + throw e; + } } - } - }); + }); + } catch (err) { + res.status(500).json({ message: 'Could not get ArcGIS layer info', error: err }); + } }) return routes } diff --git a/plugins/arcgis/web-app/projects/main/src/lib/ArcGISConfig.ts b/plugins/arcgis/web-app/projects/main/src/lib/ArcGISConfig.ts index df860dfdc..be99ed10b 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/ArcGISConfig.ts +++ b/plugins/arcgis/web-app/projects/main/src/lib/ArcGISConfig.ts @@ -13,6 +13,11 @@ export interface FeatureServiceConfig { */ token?: string + /** + * Username and password for ArcGIS authentication + */ + auth?: ArcGISAuthConfig + /** * Create layers that don't exist */ @@ -72,6 +77,22 @@ export interface FeatureLayerConfig { } +/** + * Contains username and password for ArcGIS server authentication. + */ +export interface ArcGISAuthConfig { + + /** + * The username for authentication. + */ + username: string + + /** + * The password for authentication. + */ + password: string +} + /** * Attribute configurations */ diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts index 330f54faf..3d5b0d588 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts @@ -51,9 +51,16 @@ export class ArcLayerComponent implements OnInit { onEditLayer(arcService: FeatureServiceConfig) { console.log('Editing layer ' + arcService.url + ', token: ' + arcService.token) + if (arcService.token) { + this.currentUrl = this.addToken(arcService.url, arcService.token); + } else if (arcService.auth?.username && arcService.auth?.password) { + this.currentUrl = this.addCredentials(arcService.url, arcService.auth?.username, arcService.auth?.password); + } else { + throw new Error('Invalid layer config, auth credentials: ' + JSON.stringify(arcService)) + } + this.arcLayerControl.setValue(arcService.url) this.arcTokenControl.setValue(arcService.token != null ? arcService.token : '') - this.currentUrl = this.addToken(arcService.url, arcService.token) this.layers = [] let selectedLayers = new Array() for (const layer of arcService.layers) { @@ -71,8 +78,21 @@ export class ArcLayerComponent implements OnInit { return this.layers.length == 0; } - inputChanged(layerUrl: string, token?: string) { - const url = this.addToken(layerUrl, token); + inputChanged(layerUrl: string, token?: string, username?: string, password?: string) { + let url: string; + //TODO - switch to username/pw being in body and avoid in url query + //TODO - remove hardcoded username/pw and update UI to provide + username = 'username_example' + password = 'password_example' + + if (token) { + url = this.addToken(layerUrl, token); + } else if (username && password) { + url = this.addCredentials(layerUrl, username, password); + } else { + url = layerUrl; + } + //TODO - avoid logging plain text password console.log('Input changed ' + url); if (this.timeoutId !== undefined) { window.clearTimeout(this.timeoutId); @@ -131,58 +151,104 @@ export class ArcLayerComponent implements OnInit { this.arcService.putArcConfig(this.config); } - onAddLayerUrl(layerUrl: string, layers: ArcLayerSelectable[]) { + + // Define the overloads + onAddLayerUrl(layerUrl: string, layerToken: string, layers: ArcLayerSelectable[]): void; + onAddLayerUrl(layerUrl: string, username: string, password: string, layers: ArcLayerSelectable[]): void; + + // Implement the function + /** + * Adds a new layer to the configuration if it does not already exist. + * + * @param layerUrl - The URL of the layer to be added. + * @param arg2 - Either the username for authentication or the token for the layer. + * @param arg3 - Either the password for authentication or an array of selectable layers. + * @param arg4 - Optional array of selectable layers if `arg3` is a string (username). + * + * This method performs the following steps: + * 1. Checks if the layer already exists in the configuration. + * 2. If the layer does not exist, it logs the addition of the layer. + * 3. Authenticates and retrieves a token if `arg3` is a string (password). + * 4. Creates a new feature layer configuration. + * 5. Adds selected layers to the feature layer configuration. + * 6. Updates the configuration and emits the change. + * 7. Persists the updated configuration using `arcService`. + */ + onAddLayerUrl(layerUrl: string, arg2: string, arg3: string | ArcLayerSelectable[], arg4?: ArcLayerSelectable[]): void { let serviceConfigToEdit = null; + + // Search if the layer in config to edit for (const service of this.config.featureServices) { if (service.url == layerUrl) { serviceConfigToEdit = service; } } + // Determine if layers in 3rd or 4th argument + const layers = typeof arg3 === 'string' ? arg4 : arg3; + // Add layer if it doesn't exist if (serviceConfigToEdit == null) { - console.log('Adding layer ' + layerUrl) - const splitUrl = layerUrl.split('?'); - const justUrl = splitUrl[0]; - const params = splitUrl[1]; - const urlParams = new URLSearchParams(params); - const token = urlParams.get('token'); - console.log('token is ' + token); - const featureLayer = { - url: justUrl, - token: token, + console.log('Adding layer ' + layerUrl); + let token: string | null = null; + + const featureLayer: FeatureServiceConfig = { + url: layerUrl, + token: undefined, + auth: { + username: '', + password: '' + }, layers: [] } as FeatureServiceConfig; - for (const aLayer of layers) { - if (aLayer.isSelected) { - const layerConfig = { - layer: aLayer.name, - events: JSON.parse(JSON.stringify(this.events)) + + if (typeof arg3 === 'string') { + // Handle username and password case + featureLayer.auth = { username: arg2, password: arg3 }; + } else { + // Handle token case + featureLayer.token = arg2; + } + + if (layers) { + for (const aLayer of layers) { + if (aLayer.isSelected) { + const layerConfig = { + layer: aLayer.name, + events: JSON.parse(JSON.stringify(this.events)) + } + featureLayer.layers.push(layerConfig); } - featureLayer.layers.push(layerConfig); } + + // Add the new featureLayer to the config + this.config.featureServices.push(featureLayer); + this.configChanged.emit(this.config); + this.arcService.putArcConfig(this.config); } - this.config.featureServices.push(featureLayer); - } else { + + } else { // Edit existing layer console.log('Saving edited layer ' + layerUrl) const editedLayers = []; - for (const aLayer of layers) { - if (aLayer.isSelected) { - let layerConfig = null - if (serviceConfigToEdit.layers != null) { - const index = serviceConfigToEdit.layers.findIndex((element) => { - return element.layer === aLayer.name; - }) - if (index != -1) { - layerConfig = serviceConfigToEdit.layers[index] + if (layers) { + for (const aLayer of layers) { + if (aLayer.isSelected) { + let layerConfig = null + if (serviceConfigToEdit.layers != null) { + const index = serviceConfigToEdit.layers.findIndex((element) => { + return element.layer === aLayer.name; + }) + if (index != -1) { + layerConfig = serviceConfigToEdit.layers[index] + } } - } - if (layerConfig == null) { - layerConfig = { - layer: aLayer.name, - events: JSON.parse(JSON.stringify(this.events)) + if (layerConfig == null) { + layerConfig = { + layer: aLayer.name, + events: JSON.parse(JSON.stringify(this.events)) + } } + editedLayers.push(layerConfig); } - editedLayers.push(layerConfig); } } serviceConfigToEdit.layers = editedLayers @@ -195,16 +261,19 @@ export class ArcLayerComponent implements OnInit { addToken(url: string, token?: string) { let newUrl = url if (token != null && token.length > 0) { - const index = url.indexOf('?') - let separator = '' - if (index == -1) { - separator = '?' - } else if (index < url.length - 1) { - separator = '&' - } - newUrl += separator + 'token=' + token + // TODO - appending to query param featureURL because there is an outer url using ? separater already + // const separator = url.includes('?') ? '&' : '?'; + // newUrl += separator + 'token=' + token + newUrl += '&' + 'token=' + token } return newUrl } + // Helper method to add credentials to the URL + private addCredentials(layerUrl: string, username: string, password: string): string { + const encodedUsername = encodeURIComponent(username); + const encodedPassword = encodeURIComponent(password); + // append to layerURL query parameter, outer url already contains ? separator + return `${layerUrl}&username=${encodedUsername}&password=${encodedPassword}`; + } } \ No newline at end of file