diff --git a/client/src/plugins/camunda-plugin/deployment-tool/DeploymentTool.js b/client/src/plugins/camunda-plugin/deployment-tool/DeploymentTool.js index 26d1d8df21..ed1a5820f2 100644 --- a/client/src/plugins/camunda-plugin/deployment-tool/DeploymentTool.js +++ b/client/src/plugins/camunda-plugin/deployment-tool/DeploymentTool.js @@ -14,7 +14,8 @@ import { omit } from 'min-dash'; import classNames from 'classnames'; -import { default as CamundaAPI, ApiErrors, ConnectionError, DeploymentError } from '../shared/CamundaAPI'; +import { default as CamundaAPI, DeploymentError } from '../shared/CamundaAPI'; +import { ConnectionError, GenericApiErrors } from '../shared/RestAPI'; import AUTH_TYPES from '../shared/AuthTypes'; import CockpitDeploymentLink from '../shared/ui/CockpitDeploymentLink'; @@ -541,9 +542,9 @@ export default class DeploymentTool extends PureComponent { const { code } = result; - return (code !== ApiErrors.NO_INTERNET_CONNECTION && - code !== ApiErrors.CONNECTION_FAILED && - code !== ApiErrors.NOT_FOUND); + return (code !== GenericApiErrors.NO_INTERNET_CONNECTION && + code !== GenericApiErrors.CONNECTION_FAILED && + code !== GenericApiErrors.NOT_FOUND); } closeOverlay(overlayState) { diff --git a/client/src/plugins/camunda-plugin/deployment-tool/__tests__/DeploymentToolSpec.js b/client/src/plugins/camunda-plugin/deployment-tool/__tests__/DeploymentToolSpec.js index c4cc13917c..b0e78d22cc 100644 --- a/client/src/plugins/camunda-plugin/deployment-tool/__tests__/DeploymentToolSpec.js +++ b/client/src/plugins/camunda-plugin/deployment-tool/__tests__/DeploymentToolSpec.js @@ -21,8 +21,8 @@ import { Config } from './../../../../app/__tests__/mocks'; import DeploymentTool from '../DeploymentTool'; import AUTH_TYPES from '../../shared/AuthTypes'; -import { DeploymentError, - ConnectionError } from '../../shared/CamundaAPI'; +import { DeploymentError } from '../../shared/CamundaAPI'; +import { ConnectionError } from '../../shared/RestAPI'; import { Slot, SlotFillRoot } from '../../../../app/slot-fill'; const CONFIG_KEY = 'deployment-tool'; diff --git a/client/src/plugins/camunda-plugin/shared/CamundaAPI.js b/client/src/plugins/camunda-plugin/shared/CamundaAPI.js index 359da3cac2..a66cfd34ef 100644 --- a/client/src/plugins/camunda-plugin/shared/CamundaAPI.js +++ b/client/src/plugins/camunda-plugin/shared/CamundaAPI.js @@ -10,20 +10,12 @@ import AUTH_TYPES from './AuthTypes'; -import debug from 'debug'; +import RestAPI, { ConnectionError, GenericApiErrorMessages, getNetworkErrorCode, getResponseErrorCode } from './RestAPI'; -const FETCH_TIMEOUT = 5000; - -const log = debug('CamundaAPI'); - - -export default class CamundaAPI { +export default class CamundaAPI extends RestAPI { constructor(endpoint) { - - this.baseUrl = normalizeBaseURL(endpoint.url); - - this.authentication = this.getAuthentication(endpoint); + super('CamundaAPI', normalizeBaseURL(endpoint.url), getAuthentication(endpoint)); } async deployDiagram(diagram, deployment) { @@ -130,152 +122,40 @@ export default class CamundaAPI { throw new ConnectionError(response); } +} - getAuthentication(endpoint) { - - const { - authType, - username, - password, - token - } = endpoint; - - switch (authType) { - case AUTH_TYPES.BASIC: - return { - username, - password - }; - case AUTH_TYPES.BEARER: - return { - token - }; - } - } - - getHeaders() { - const headers = { - accept: 'application/json' - }; - - if (this.authentication) { - headers.authorization = this.getAuthHeader(this.authentication); - } - - return headers; - } +function getAuthentication(endpoint) { - getAuthHeader(endpoint) { + const { + authType, + username, + password, + token + } = endpoint; - const { - token, + switch (authType) { + case AUTH_TYPES.BASIC: + return { username, password - } = endpoint; - - if (token) { - return `Bearer ${token}`; - } - - if (username && password) { - const credentials = window.btoa(`${username}:${password}`); - - return `Basic ${credentials}`; - } - } - - async fetch(path, options = {}) { - const url = `${this.baseUrl}${path}`; - const headers = { - ...options.headers, - ...this.getHeaders() }; - - try { - const signal = options.signal || this.setupTimeoutSignal(); - - return await fetch(url, { - ...options, - headers, - signal - }); - } catch (error) { - log('failed to fetch', error); - - return { - url, - json: () => { - return {}; - } - }; - } - } - - setupTimeoutSignal(timeout = FETCH_TIMEOUT) { - const controller = new AbortController(); - - setTimeout(() => controller.abort(), timeout); - - return controller.signal; - } - - async parse(response) { - try { - const json = await response.json(); - - return json; - } catch (error) { - return {}; - } + case AUTH_TYPES.BEARER: + return { + token + }; } } -const NO_INTERNET_CONNECTION = 'NO_INTERNET_CONNECTION'; -const CONNECTION_FAILED = 'CONNECTION_FAILED'; const DIAGRAM_PARSE_ERROR = 'DIAGRAM_PARSE_ERROR'; -const UNAUTHORIZED = 'UNAUTHORIZED'; -const FORBIDDEN = 'FORBIDDEN'; -const NOT_FOUND = 'NOT_FOUND'; -const INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR'; -const UNAVAILABLE_ERROR = 'UNAVAILABLE_ERROR'; export const ApiErrors = { - NO_INTERNET_CONNECTION, - CONNECTION_FAILED, DIAGRAM_PARSE_ERROR, - UNAUTHORIZED, - FORBIDDEN, - NOT_FOUND, - INTERNAL_SERVER_ERROR, - UNAVAILABLE_ERROR }; export const ApiErrorMessages = { - [ NO_INTERNET_CONNECTION ]: 'Could not establish a network connection.', - [ CONNECTION_FAILED ]: 'Should point to a running Camunda REST API.', [ DIAGRAM_PARSE_ERROR ]: 'Server could not parse the diagram. Please check log for errors.', - [ UNAUTHORIZED ]: 'Credentials do not match with the server.', - [ FORBIDDEN ]: 'This user is not permitted to deploy. Please use different credentials or get this user enabled to deploy.', - [ NOT_FOUND ]: 'Should point to a running Camunda REST API.', - [ INTERNAL_SERVER_ERROR ]: 'Camunda is reporting an error. Please check the server status.', - [ UNAVAILABLE_ERROR ]: 'Camunda is reporting an error. Please check the server status.' }; -export class ConnectionError extends Error { - - constructor(response) { - super('Connection failed'); - - this.code = ( - getResponseErrorCode(response) || - getNetworkErrorCode(response) - ); - - this.details = ApiErrorMessages[this.code]; - } -} - - export class DeploymentError extends Error { constructor(response, body) { @@ -287,7 +167,7 @@ export class DeploymentError extends Error { getNetworkErrorCode(response) ); - this.details = ApiErrorMessages[this.code]; + this.details = ApiErrorMessages[this.code] || GenericApiErrorMessages[this.code]; this.problems = body && body.message; } @@ -304,7 +184,7 @@ export class StartInstanceError extends Error { getNetworkErrorCode(response) ); - this.details = ApiErrorMessages[this.code]; + this.details = ApiErrorMessages[this.code] || GenericApiErrorMessages[this.code]; this.problems = body && body.message; } @@ -313,29 +193,6 @@ export class StartInstanceError extends Error { // helpers /////////////// -function getNetworkErrorCode(response) { - if (isLocalhost(response.url) || isOnline()) { - return CONNECTION_FAILED; - } - - return NO_INTERNET_CONNECTION; -} - -function getResponseErrorCode(response) { - switch (response.status) { - case 401: - return UNAUTHORIZED; - case 403: - return FORBIDDEN; - case 404: - return NOT_FOUND; - case 500: - return INTERNAL_SERVER_ERROR; - case 503: - return UNAVAILABLE_ERROR; - } -} - function getCamundaErrorCode(response, body) { const PARSE_ERROR_PREFIX = 'ENGINE-09005 Could not parse BPMN process.'; @@ -345,14 +202,6 @@ function getCamundaErrorCode(response, body) { } } -function isLocalhost(url) { - return /^https?:\/\/(127\.0\.0\.1|localhost)/.test(url); -} - -function isOnline() { - return window.navigator.onLine; -} - function normalizeBaseURL(url) { return url.replace(/\/deployment\/create\/?/, ''); } diff --git a/client/src/plugins/camunda-plugin/shared/RestAPI.js b/client/src/plugins/camunda-plugin/shared/RestAPI.js new file mode 100644 index 0000000000..2330952bd8 --- /dev/null +++ b/client/src/plugins/camunda-plugin/shared/RestAPI.js @@ -0,0 +1,170 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +import debug from 'debug'; + +const DEFAULT_FETCH_TIMEOUT = 5000; + +export default class RestAPI { + + constructor(apiLoggerName, baseUrl, authentication) { + this.log = debug(apiLoggerName); + this.baseUrl = baseUrl; + this.authentication = authentication; + } + + async fetch(path, options = {}) { + const url = `${this.baseUrl}${path}`; + const headers = { + ...options.headers, + ...this.getHeaders() + }; + + try { + const signal = options.signal || this.setupTimeoutSignal(); + + return await fetch(url, { + ...options, + headers, + signal + }); + } catch (error) { + this.log('failed to fetch', error); + + return { + url, + json: () => { + return {}; + } + }; + } + } + + getHeaders() { + const headers = { + accept: 'application/json' + }; + + if (this.authentication) { + headers.authorization = this.getAuthHeader(this.authentication); + } + + return headers; + } + + getAuthHeader(endpoint) { + const { + token, + username, + password + } = endpoint; + + if (token) { + return `Bearer ${token}`; + } + + if (username && password) { + const credentials = window.btoa(`${username}:${password}`); + + return `Basic ${credentials}`; + } + } + + setupTimeoutSignal(timeout = DEFAULT_FETCH_TIMEOUT) { + const controller = new AbortController(); + + setTimeout(() => controller.abort(), timeout); + + return controller.signal; + } + + async parse(response) { + try { + const json = await response.json(); + + return json; + } catch (error) { + return {}; + } + } +} + +const NO_INTERNET_CONNECTION = 'NO_INTERNET_CONNECTION'; +const CONNECTION_FAILED = 'CONNECTION_FAILED'; +const UNAUTHORIZED = 'UNAUTHORIZED'; +const FORBIDDEN = 'FORBIDDEN'; +const NOT_FOUND = 'NOT_FOUND'; +const INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR'; +const UNAVAILABLE_ERROR = 'UNAVAILABLE_ERROR'; + +export const GenericApiErrors = { + NO_INTERNET_CONNECTION, + CONNECTION_FAILED, + UNAUTHORIZED, + FORBIDDEN, + NOT_FOUND, + INTERNAL_SERVER_ERROR, + UNAVAILABLE_ERROR +}; + +export const GenericApiErrorMessages = { + [ NO_INTERNET_CONNECTION ]: 'Could not establish a network connection.', + [ CONNECTION_FAILED ]: 'Should point to a running Camunda REST API.', + [ UNAUTHORIZED ]: 'Credentials do not match with the server.', + [ FORBIDDEN ]: 'This user is not permitted to deploy. Please use different credentials or get this user enabled to deploy.', + [ NOT_FOUND ]: 'Should point to a running Camunda REST API.', + [ INTERNAL_SERVER_ERROR ]: 'Camunda is reporting an error. Please check the server status.', + [ UNAVAILABLE_ERROR ]: 'Camunda is reporting an error. Please check the server status.' +}; + +export class ConnectionError extends Error { + + constructor(response) { + super('Connection failed'); + + this.code = ( + getResponseErrorCode(response) || + getNetworkErrorCode(response) + ); + + this.details = GenericApiErrorMessages[this.code]; + } +} + +export function getNetworkErrorCode(response) { + if (isLocalhost(response.url) || isOnline()) { + return CONNECTION_FAILED; + } + + return NO_INTERNET_CONNECTION; +} + +export function getResponseErrorCode(response) { + switch (response.status) { + case 401: + return UNAUTHORIZED; + case 403: + return FORBIDDEN; + case 404: + return NOT_FOUND; + case 500: + return INTERNAL_SERVER_ERROR; + case 503: + return UNAVAILABLE_ERROR; + } +} + +function isLocalhost(url) { + return /^https?:\/\/(127\.0\.0\.1|localhost)/.test(url); +} + +function isOnline() { + return window.navigator.onLine; +} diff --git a/client/src/plugins/camunda-plugin/shared/WellKnownAPI.js b/client/src/plugins/camunda-plugin/shared/WellKnownAPI.js index 15dc50e482..e41f4f5ed6 100644 --- a/client/src/plugins/camunda-plugin/shared/WellKnownAPI.js +++ b/client/src/plugins/camunda-plugin/shared/WellKnownAPI.js @@ -8,26 +8,23 @@ * except in compliance with the MIT License. */ -import debug from 'debug'; +import RestAPI, { ConnectionError } from './RestAPI'; -const FETCH_TIMEOUT = 5000; - -const log = debug('WellKnownAPI'); +const WELL_KNOWN_PATH_WEBAPPS = '/.well-known/camunda-automation-platform/webapps'; export function forEngineRestUrl(engineRestUrl) { const engineUrl = new URL(engineRestUrl); return new WellKnownAPI(`${engineUrl.protocol}//${engineUrl.host}`); } - -export default class WellKnownAPI { +export default class WellKnownAPI extends RestAPI { constructor(url) { - this.baseUrl = url; + super('WellKnownAPI', url); } async getWellKnownWebAppUrls() { - const response = await this.fetch('/.well-known/camunda-automation-platform/webapps'); + const response = await this.fetch(WELL_KNOWN_PATH_WEBAPPS); if (response.ok) { const { @@ -83,123 +80,4 @@ export default class WellKnownAPI { // in case we got the root path, we assume its the default process engine .replace(new RegExp(`${appName}/$`), `${appName}/default/#/`); } - - getHeaders() { - const headers = { - accept: 'application/json' - }; - - return headers; - } - - setupTimeoutSignal(timeout = FETCH_TIMEOUT) { - const controller = new AbortController(); - - setTimeout(() => controller.abort(), timeout); - - return controller.signal; - } - - async fetch(path, options = {}) { - const url = `${this.baseUrl}${path}`; - const headers = { - ...options.headers, - ...this.getHeaders() - }; - - try { - const signal = options.signal || this.setupTimeoutSignal(); - - return await fetch(url, { - ...options, - headers, - signal - }); - } catch (error) { - log('failed to fetch', error); - - return { - url, - json: () => { - return {}; - } - }; - } - } -} - -const NO_INTERNET_CONNECTION = 'NO_INTERNET_CONNECTION'; -const CONNECTION_FAILED = 'CONNECTION_FAILED'; -const UNAUTHORIZED = 'UNAUTHORIZED'; -const FORBIDDEN = 'FORBIDDEN'; -const NOT_FOUND = 'NOT_FOUND'; -const INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR'; -const UNAVAILABLE_ERROR = 'UNAVAILABLE_ERROR'; - -export const ApiErrors = { - NO_INTERNET_CONNECTION, - CONNECTION_FAILED, - UNAUTHORIZED, - FORBIDDEN, - NOT_FOUND, - INTERNAL_SERVER_ERROR, - UNAVAILABLE_ERROR -}; - -export const ApiErrorMessages = { - [ NO_INTERNET_CONNECTION ]: 'Could not establish a network connection.', - [ CONNECTION_FAILED ]: 'Should point to a running REST API.', - [ UNAUTHORIZED ]: 'Credentials do not match with the server.', - [ FORBIDDEN ]: 'This user is not permitted to retrieve well-known info. Please use different credentials or get this user enabled.', - [ NOT_FOUND ]: 'The server does not provide a well-known endpoint.', - [ INTERNAL_SERVER_ERROR ]: 'Server is reporting an error. Please check the server status.', - [ UNAVAILABLE_ERROR ]: 'Server is reporting an error. Please check the server status.' -}; - -export class ConnectionError extends Error { - - constructor(response) { - super('Connection failed'); - - this.code = ( - getResponseErrorCode(response) || - getNetworkErrorCode(response) - ); - - this.details = ApiErrorMessages[this.code]; - } -} - - -// helpers /////////////// - -function getNetworkErrorCode(response) { - if (isLocalhost(response.url) || isOnline()) { - return CONNECTION_FAILED; - } - - return NO_INTERNET_CONNECTION; -} - -function getResponseErrorCode(response) { - switch (response.status) { - case 401: - return UNAUTHORIZED; - case 403: - return FORBIDDEN; - case 404: - return NOT_FOUND; - case 500: - return INTERNAL_SERVER_ERROR; - case 503: - return UNAVAILABLE_ERROR; - } -} - -function isLocalhost(url) { - return /^https?:\/\/(127\.0\.0\.1|localhost)/.test(url); -} - -function isOnline() { - return window.navigator.onLine; } diff --git a/client/src/plugins/camunda-plugin/shared/__tests__/WellKnownAPISpec.js b/client/src/plugins/camunda-plugin/shared/__tests__/WellKnownAPISpec.js index 044912296a..90b97a3151 100644 --- a/client/src/plugins/camunda-plugin/shared/__tests__/WellKnownAPISpec.js +++ b/client/src/plugins/camunda-plugin/shared/__tests__/WellKnownAPISpec.js @@ -10,7 +10,8 @@ /* global sinon */ -import WellKnownAPI, { ConnectionError } from '../WellKnownAPI'; +import { ConnectionError } from '../RestAPI'; +import WellKnownAPI from '../WellKnownAPI'; describe('', () => { @@ -37,9 +38,9 @@ describe('', () => { fetchSpy.resolves(new Response({ json: () => ({ - 'admin': 'http://localhost:18080/camunda/apps/admin/default/#/', - 'tasklist': 'http://localhost:18080/camunda/apps/tasklist/default/#/', - 'cockpit': 'http://localhost/camunda/apps/cockpit/default/#/' + 'adminUrl': 'http://localhost:18080/camunda/apps/admin/default/#/', + 'tasklistUrl': 'http://localhost:18080/camunda/apps/tasklist/default/#/', + 'cockpitUrl': 'http://localhost/camunda/apps/cockpit/default/#/' }), })); @@ -153,9 +154,9 @@ describe('', () => { resolve(new Response({ json: () => ({ - 'admin': 'http://localhost:18080/camunda/apps/admin/default/#/', - 'tasklist': 'http://localhost:18080/camunda/apps/tasklist/default/#/', - 'cockpit': 'http://localhost/camunda/apps/cockpit/default/#/' + 'adminUrl': 'http://localhost:18080/camunda/apps/admin/default/#/', + 'tasklistUrl': 'http://localhost:18080/camunda/apps/tasklist/default/#/', + 'cockpitUrl': 'http://localhost/camunda/apps/cockpit/default/#/' }), })); }); @@ -189,7 +190,7 @@ describe('', () => { fetchSpy.resolves(new Response({ json: () => ({ - 'cockpit': 'http://localhost/camunda/apps/cockpit/default/#/' + 'cockpitUrl': 'http://localhost/camunda/apps/cockpit/default/#/' }), })); @@ -208,7 +209,7 @@ describe('', () => { fetchSpy.resolves(new Response({ json: () => ({ - 'cockpit': 'http://localhost/camunda/apps/cockpit/default/#/' + 'cockpitUrl': 'http://localhost/camunda/apps/cockpit/default/#/' }), })); @@ -227,7 +228,7 @@ describe('', () => { fetchSpy.resolves(new Response({ json: () => ({ - 'cockpit': 'http://localhost/camunda/apps/cockpit/default/#/' + 'cockpitUrl': 'http://localhost/camunda/apps/cockpit/default/#/' }), })); @@ -339,7 +340,7 @@ describe('', () => { resolve(new Response({ json: () => ({ - 'cockpit': 'http://localhost/camunda/apps/cockpit/default/#/' + 'cockpitUrl': 'http://localhost/camunda/apps/cockpit/default/#/' }), })); }); diff --git a/client/src/plugins/camunda-plugin/shared/__tests__/webAppUrlsSpec.js b/client/src/plugins/camunda-plugin/shared/__tests__/webAppUrlsSpec.js index ebc1b03664..c16dad3568 100644 --- a/client/src/plugins/camunda-plugin/shared/__tests__/webAppUrlsSpec.js +++ b/client/src/plugins/camunda-plugin/shared/__tests__/webAppUrlsSpec.js @@ -10,8 +10,9 @@ /* global sinon */ +import { ConnectionError } from '../RestAPI'; import { determineCockpitUrl } from '../webAppUrls'; -import WellKnownAPI, { ConnectionError } from '../WellKnownAPI'; +import WellKnownAPI from '../WellKnownAPI'; let getCockpitUrlStub; diff --git a/client/src/plugins/camunda-plugin/start-instance-tool/StartInstanceTool.js b/client/src/plugins/camunda-plugin/start-instance-tool/StartInstanceTool.js index 6be1d20906..bb68886fbe 100644 --- a/client/src/plugins/camunda-plugin/start-instance-tool/StartInstanceTool.js +++ b/client/src/plugins/camunda-plugin/start-instance-tool/StartInstanceTool.js @@ -12,7 +12,8 @@ import React, { PureComponent } from 'react'; import PlayIcon from 'icons/Play.svg'; -import CamundaAPI, { ConnectionError, DeploymentError, StartInstanceError } from '../shared/CamundaAPI'; +import CamundaAPI, { DeploymentError, StartInstanceError } from '../shared/CamundaAPI'; +import { ConnectionError } from '../shared/RestAPI'; import StartInstanceConfigOverlay from './StartInstanceConfigOverlay'; diff --git a/client/src/plugins/camunda-plugin/start-instance-tool/__tests__/StartInstanceToolSpec.js b/client/src/plugins/camunda-plugin/start-instance-tool/__tests__/StartInstanceToolSpec.js index a359cb3d4d..8b32d0e52c 100644 --- a/client/src/plugins/camunda-plugin/start-instance-tool/__tests__/StartInstanceToolSpec.js +++ b/client/src/plugins/camunda-plugin/start-instance-tool/__tests__/StartInstanceToolSpec.js @@ -25,10 +25,11 @@ import StartInstanceTool from '../StartInstanceTool'; import { DeploymentError, - ConnectionError, StartInstanceError } from '../../shared/CamundaAPI'; +import { ConnectionError } from '../../shared/RestAPI'; + import { Slot, SlotFillRoot