UNPKG

passbolt-styleguide

Version:

Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.

420 lines (396 loc) 14.9 kB
/** * Passbolt ~ Open source password manager for teams * Copyright (c) Passbolt SA (https://www.passbolt.com) * * Licensed under GNU Affero General Public License version 3 of the or any later version. * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) * @license https://opensource.org/licenses/AGPL-3.0 AGPL License * @link https://www.passbolt.com Passbolt(tm) * @since 2.13.0 */ import PassboltApiFetchError from "../Error/PassboltApiFetchError"; import PassboltBadResponseError from "../Error/PassboltBadResponseError"; import PassboltServiceUnavailableError from "../Error/PassboltServiceUnavailableError"; const SUPPORTED_METHODS = ["GET", "POST", "PUT", "DELETE"]; export class ApiClient { /** * Constructor * * @param {ApiClientOptions} options * @throws {TypeError} if baseUrl is empty or not a string * @public */ constructor(options) { this.options = options; if (!this.options.getBaseUrl()) { throw new TypeError("ApiClient constructor error: baseUrl is required."); } if (!this.options.getResourceName()) { throw new TypeError("ApiClient constructor error: resourceName is required."); } try { let rawBaseUrl = this.options.getBaseUrl().toString(); if (rawBaseUrl.endsWith("/")) { rawBaseUrl = rawBaseUrl.slice(0, -1); } let resourceName = this.options.getResourceName(); if (resourceName.startsWith("/")) { resourceName = resourceName.slice(1); } if (resourceName.endsWith("/")) { resourceName = resourceName.slice(0, -1); } this.baseUrl = `${rawBaseUrl}/${resourceName}`; this.baseUrl = new URL(this.baseUrl); } catch (error) { throw new TypeError("ApiClient constructor error: b.", { cause: error }); } this.apiVersion = "api-version=v2"; } /** * @returns {Object} fetchOptions.headers * @private */ getDefaultHeaders() { return { Accept: "application/json", "content-type": "application/json", }; } /** * @returns {Promise<Object>} fetchOptions * @private */ async buildFetchOptions() { const optionHeaders = await this.options.getHeaders(); return { credentials: "include", headers: { ...this.getDefaultHeaders(), ...optionHeaders }, }; } /** * Find a resource by id * * @param {string} id most likely a uuid * @param {Object} [urlOptions] Optional url parameters for example {"contain[something]": "1"} * @throws {TypeError} if id is empty or not a string * @throws {TypeError} if urlOptions key or values are not a string * @throws {PassboltServiceUnavailableError} if service is not reachable * @throws {PassboltBadResponseError} if passbolt API responded with non parsable JSON * @throws {PassboltApiFetchError} if passbolt API response is not OK (non 2xx status) * @returns {Promise<*>} * @public */ async get(id, urlOptions) { this.assertValidId(id); const url = this.buildUrl(`${this.baseUrl}/${id}`, urlOptions || {}); return this.fetchAndHandleResponse("GET", url); } /** * Delete a resource by id * * @param {string} id most likely a uuid * @param {Object} [body] (will be converted to JavaScript Object Notation (JSON) string) * @param {Object} [urlOptions] Optional url parameters for example {"contain[something]": "1"} * @param {Boolean} [dryRun] optional, default false, checks if the validity of the operation prior real delete * @throws {TypeError} if id is empty or not a string * @throws {TypeError} if urlOptions key or values are not a string * @throws {PassboltServiceUnavailableError} if service is not reachable * @throws {PassboltBadResponseError} if passbolt API responded with non parsable JSON * @throws {PassboltApiFetchError} if passbolt API response is not OK (non 2xx status) * @returns {Promise<*>} * @public */ async delete(id, body, urlOptions, dryRun) { this.assertValidId(id); let url; if (typeof dryRun === "undefined") { dryRun = false; } if (!dryRun) { url = this.buildUrl(`${this.baseUrl}/${id}`, urlOptions || {}); } else { url = this.buildUrl(`${this.baseUrl}/${id}/dry-run`, urlOptions || {}); } let bodyString = null; if (body) { bodyString = this.buildBody(body); } return this.fetchAndHandleResponse("DELETE", url, bodyString); } /** * Find all the resources * * @param {Object} [urlOptions] Optional url parameters for example {"contain[something]": "1"} * @throws {TypeError} if urlOptions key or values are not a string * @throws {PassboltServiceUnavailableError} if service is not reachable * @throws {PassboltBadResponseError} if passbolt API responded with non parsable JSON * @throws {PassboltApiFetchError} if passbolt API response is not OK (non 2xx status) * @returns {Promise<*>} * @public */ async findAll(urlOptions) { const url = this.buildUrl(this.baseUrl.toString(), urlOptions || {}); return this.fetchAndHandleResponse("GET", url); } /** * Create a resource * * @param {Object} body (will be converted to JavaScript Object Notation (JSON) string) * @param {Object} [urlOptions] Optional url parameters for example {"contain[something]": "1"} * @throws {TypeError} if body is empty or cannot converted to valid JSON string * @throws {PassboltServiceUnavailableError} if service is not reachable * @throws {PassboltBadResponseError} if passbolt API responded with non parsable JSON * @throws {PassboltApiFetchError} if passbolt API response is not OK (non 2xx status) * @returns {Promise<*>} * @public */ async create(body, urlOptions) { const url = this.buildUrl(this.baseUrl.toString(), urlOptions || {}); const bodyString = this.buildBody(body); return this.fetchAndHandleResponse("POST", url, bodyString); } /** * Update a resource * * @param {string} id most likely a uuid * @param {Object} body (will be converted to JavaScript Object Notation (JSON) string) * @param {Object} [urlOptions] Optional url parameters for example {"contain[something]": "1"} * @param {Boolean?} [dryRun] optional, default false, checks if the validity of the operation prior real update * @throws {TypeError} if id is empty or not a string * @throws {TypeError} if body is empty or cannot converted to valid JSON string * @throws {PassboltServiceUnavailableError} if service is not reachable * @throws {PassboltBadResponseError} if passbolt API responded with non parsable JSON * @throws {PassboltApiFetchError} if passbolt API response is not OK (non 2xx status) * @returns {Promise<*>} * @public */ async update(id, body, urlOptions, dryRun) { this.assertValidId(id); let url; if (typeof dryRun === "undefined") { dryRun = false; } if (!dryRun) { url = this.buildUrl(`${this.baseUrl}/${id}`, urlOptions || {}); } else { url = this.buildUrl(`${this.baseUrl}/${id}/dry-run`, urlOptions || {}); } let bodyString = null; if (body) { bodyString = this.buildBody(body); } return this.fetchAndHandleResponse("PUT", url, bodyString); } /** * Update all. * * @param {Object} body (will be converted to JavaScript Object Notation (JSON) string) * @param {Object} [urlOptions] Optional url parameters for example {"contain[something]": "1"} * @throws {TypeError} if body is empty or cannot converted to valid JSON string * @throws {PassboltServiceUnavailableError} if service is not reachable * @throws {PassboltBadResponseError} if passbolt API responded with non parsable JSON * @throws {PassboltApiFetchError} if passbolt API response is not OK (non 2xx status) * @returns {Promise<*>} * @public */ async updateAll(body, urlOptions = {}) { const url = this.buildUrl(this.baseUrl.toString(), urlOptions); const bodyString = body ? this.buildBody(body) : null; return this.fetchAndHandleResponse("PUT", url, bodyString); } /** * Assert that an id is a valid non empty string * * @throws {TypeError} if id is empty or not a string * @param {string} id * @return {void} * @private */ assertValidId(id) { if (!id) { throw new TypeError("ApiClient.assertValidId error: id cannot be empty"); } if (typeof id !== "string") { throw new TypeError("ApiClient.assertValidId error: id should be a string"); } } /** * @throw TypeError * @param method * @private */ assertMethod(method) { if (typeof method !== "string") { throw new TypeError("ApiClient.assertValidMethod method should be a string."); } if (SUPPORTED_METHODS.indexOf(method.toUpperCase()) < 0) { throw new TypeError(`ApiClient.assertValidMethod error: method ${method} is not supported.`); } } /** * Url paramter assertion * @param {*} url * @throw TypeError * @private */ assertUrl(url) { if (!url) { throw new TypeError("ApliClient.assertUrl error: url is required."); } if (!(url instanceof URL)) { throw new TypeError("ApliClient.assertUrl error: url should be a valid URL object."); } if (url.protocol !== "https:" && url.protocol !== "http:") { throw new TypeError("ApliClient.assertUrl error: url protocol should only be https or http."); } } /** * Body parameter assertion * @param body * @throws {TypeError} if body is not a string * @private */ assertBody(body) { // Body form data is needed to verify the server, and sign-in a user. const isFormData = body instanceof FormData; if (!isFormData && typeof body !== "string") { throw new TypeError(`ApiClient.assertBody error: body should be a string or a FormData.`); } } /** * Build body object * * @param {Object} body * @throws {TypeError} if body is empty or cannot converted to valid JSON string * @return {string} JavaScript Object Notation (JSON) string * @private */ buildBody(body) { return JSON.stringify(body); } /** * Return a URL object from string url and this.baseUrl and this.apiVersion * Optionally append urlOptions to the URL object * * @param {string} url * @param {Object} [urlOptions] Optional url parameters for example {"contain[something]": "1"} * @throws {TypeError} if urlOptions key or values are not a string * @returns {URL} * @public */ buildUrl(url, urlOptions) { if (typeof url !== "string") { throw new TypeError("ApiClient.buildUrl error: url should be a string."); } const urlObj = new URL(`${url}.json?${this.apiVersion}`); urlOptions = urlOptions || {}; for (const [key, value] of Object.entries(urlOptions)) { if (typeof key !== "string") { throw new TypeError("ApiClient.buildUrl error: urlOptions key should be a string."); } if (typeof value === "string") { // Example "filter[has-tag]": "<string>" urlObj.searchParams.append(key, value); } else { // Example "filter[has-id][]": "<uuid>" if (Array.isArray(value)) { value.forEach((v) => { urlObj.searchParams.append(key, v); }); } else { throw new TypeError("ApiClient.buildUrl error: urlOptions value should be a string or array."); } } } return urlObj; } /** * Send a request to the API without handling the response * * @param {string} method example 'GET', 'POST' * @param {URL} url object * @param {*} [body] (optional) * @param {Object} [options] (optional) more fetch options * @throws {PassboltServiceUnavailableError} if service is not reachable * @returns {Promise<*>} * @public */ async sendRequest(method, url, body, options) { this.assertUrl(url); this.assertMethod(method); if (body) { this.assertBody(body); } // Determine the fetch strategy, in some cases it could use a custom fetch as for MV3 to solve the invalid certificate issue. // eslint-disable-next-line no-undef const fetchStrategy = typeof customApiClientFetch !== "undefined" ? customApiClientFetch : fetch; const builtFecthOptions = await this.buildFetchOptions(); const fetchOptions = { ...builtFecthOptions, ...options }; fetchOptions.method = method; if (body) { fetchOptions.body = body; } try { return await fetchStrategy(url.toString(), fetchOptions); } catch (error) { // Display the error in the console to see the details (maybe more details should appear in the future) console.error(error); // The error message is always failed to fetch with no details. if (navigator.onLine) { // Catch Network error such as bad certificate or server unreachable. throw new PassboltServiceUnavailableError("Unable to reach the server, an unexpected error occurred"); } else { // Network connection lost. throw new PassboltServiceUnavailableError("Unable to reach the server, you are not connected to the network"); } } } /** * fetchAndHandleResponse * * @param {string} method example 'GET', 'POST' * @param {URL} url object * @param {*} [body] (optional) * @param {Object} [options] (optional) more fetch options * @throws {TypeError} if method, url are not defined or of the wrong type * @throws {PassboltServiceUnavailableError} if service is not reachable * @throws {PassboltBadResponseError} if passbolt API responded with non parsable JSON * @throws {PassboltApiFetchError} if passbolt API response is not OK (non 2xx status) * @returns {Promise<*>} * @public */ async fetchAndHandleResponse(method, url, body, options) { const response = await this.sendRequest(method, url, body, options); return this.parseResponseJson(response); } /** * Parse the response into json * @param {Response} response Fetch response * @return {Promise<object>} */ async parseResponseJson(response) { let responseJson; try { responseJson = await response.json(); } catch (error) { console.debug(response.url.toString(), error); /* * If the response cannot be parsed, it's not a Passbolt API response. * It can be a for example a proxy timeout error (504). */ throw new PassboltBadResponseError(error, response); } if (!response.ok) { const message = responseJson.header.message; throw new PassboltApiFetchError(message, { code: response.status, body: responseJson.body, }); } return responseJson; } }