diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index 27ed5ba67..6315e5d2b 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -249,6 +249,7 @@ "no_duration": "No duration", "no_results_found": "No matches found", "page_not_found": "This page could not be found", + "proxy_error": "Proxy error", "script_fail": "Could not execute pre-request script", "something_went_wrong": "Something went wrong", "test_script_fail": "Could not execute post-request script" diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts index ba0a9fb7d..13997d2af 100644 --- a/packages/hoppscotch-common/src/components.d.ts +++ b/packages/hoppscotch-common/src/components.d.ts @@ -92,6 +92,7 @@ declare module '@vue/runtime-core' { HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'] HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder'] HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing'] + HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio'] HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup'] HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver'] HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'] @@ -160,6 +161,8 @@ declare module '@vue/runtime-core' { RealtimeLog: typeof import('./components/realtime/Log.vue')['default'] RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default'] RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default'] + SettingsExtension: typeof import('./components/settings/Extension.vue')['default'] + SettingsProxy: typeof import('./components/settings/Proxy.vue')['default'] SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default'] SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default'] SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default'] diff --git a/packages/hoppscotch-common/src/components/app/Interceptor.vue b/packages/hoppscotch-common/src/components/app/Interceptor.vue index f54dbb110..427770310 100644 --- a/packages/hoppscotch-common/src/components/app/Interceptor.vue +++ b/packages/hoppscotch-common/src/components/app/Interceptor.vue @@ -8,91 +8,41 @@ {{ t("settings.interceptor_description") }}

- -
- - + +
+
+ + + +
diff --git a/packages/hoppscotch-common/src/components/graphql/Request.vue b/packages/hoppscotch-common/src/components/graphql/Request.vue index afaefa79c..9f303b05a 100644 --- a/packages/hoppscotch-common/src/components/graphql/Request.vue +++ b/packages/hoppscotch-common/src/components/graphql/Request.vue @@ -29,7 +29,6 @@ diff --git a/packages/hoppscotch-common/src/components/settings/Extension.vue b/packages/hoppscotch-common/src/components/settings/Extension.vue new file mode 100644 index 000000000..1ccd80baa --- /dev/null +++ b/packages/hoppscotch-common/src/components/settings/Extension.vue @@ -0,0 +1,88 @@ + + + diff --git a/packages/hoppscotch-common/src/components/settings/Proxy.vue b/packages/hoppscotch-common/src/components/settings/Proxy.vue new file mode 100644 index 000000000..f5ff15ff8 --- /dev/null +++ b/packages/hoppscotch-common/src/components/settings/Proxy.vue @@ -0,0 +1,94 @@ + + + diff --git a/packages/hoppscotch-common/src/helpers/GQLConnection.ts b/packages/hoppscotch-common/src/helpers/GQLConnection.ts index 15d17b5cd..3af67815e 100644 --- a/packages/hoppscotch-common/src/helpers/GQLConnection.ts +++ b/packages/hoppscotch-common/src/helpers/GQLConnection.ts @@ -1,3 +1,4 @@ +import * as E from "fp-ts/Either" import { BehaviorSubject } from "rxjs" import { getIntrospectionQuery, @@ -11,7 +12,8 @@ import { } from "graphql" import { distinctUntilChanged, map } from "rxjs/operators" import { GQLHeader, HoppGQLAuth } from "@hoppscotch/data" -import { sendNetworkRequest } from "./network" +import { getService } from "~/modules/dioc" +import { InterceptorService } from "~/services/interceptor.service" const GQL_SCHEMA_POLL_INTERVAL = 7000 @@ -181,7 +183,7 @@ export class GQLConnection { headers.forEach((x) => (finalHeaders[x.key] = x.value)) const reqOptions = { - method: "POST", + method: "POST" as const, url, headers: { ...finalHeaders, @@ -190,11 +192,20 @@ export class GQLConnection { data: introspectionQuery, } - const data = await sendNetworkRequest(reqOptions) + const interceptorService = getService(InterceptorService) + + const res = await interceptorService.runRequest(reqOptions).response + + if (E.isLeft(res)) { + console.error(res.left) + throw new Error(res.left.toString()) + } + + const data = res.right // HACK : Temporary trailing null character issue from the extension fix const response = new TextDecoder("utf-8") - .decode(data.data) + .decode(data.data as any) .replace(/\0+$/, "") const introspectResponse = JSON.parse(response) @@ -245,7 +256,7 @@ export class GQLConnection { .forEach(({ key, value }) => (finalHeaders[key] = value)) const reqOptions = { - method: "POST", + method: "POST" as const, url, headers: { ...finalHeaders, @@ -260,11 +271,19 @@ export class GQLConnection { }, } - const res = await sendNetworkRequest(reqOptions) + const interceptorService = getService(InterceptorService) + const result = await interceptorService.runRequest(reqOptions).response + + if (E.isLeft(result)) { + console.error(result.left) + throw new Error(result.left.toString()) + } + + const res = result.right // HACK: Temporary trailing null character issue from the extension fix const responseText = new TextDecoder("utf-8") - .decode(res.data) + .decode(res.data as any) .replace(/\0+$/, "") return responseText diff --git a/packages/hoppscotch-common/src/helpers/RequestRunner.ts b/packages/hoppscotch-common/src/helpers/RequestRunner.ts index a79a95907..98666efc0 100644 --- a/packages/hoppscotch-common/src/helpers/RequestRunner.ts +++ b/packages/hoppscotch-common/src/helpers/RequestRunner.ts @@ -1,6 +1,5 @@ import { Observable, Subject } from "rxjs" import { filter } from "rxjs/operators" -import * as TE from "fp-ts/lib/TaskEither" import { flow, pipe } from "fp-ts/function" import * as O from "fp-ts/Option" import * as A from "fp-ts/Array" @@ -10,7 +9,7 @@ import { runTestScript, TestDescriptor, } from "@hoppscotch/js-sandbox" -import { isRight } from "fp-ts/Either" +import * as E from "fp-ts/Either" import { cloneDeep } from "lodash-es" import { getCombinedEnvVariables, @@ -69,106 +68,130 @@ export const executedResponses$ = new Subject< HoppRESTResponse & { type: "success" | "fail " } >() -export const runRESTRequest$ = ( +export function runRESTRequest$( tab: Ref -): TE.TaskEither> => - pipe( - getFinalEnvsFromPreRequest( - tab.value.document.request.preRequestScript, - getCombinedEnvVariables() - ), - TE.chain((envs) => { - const effectiveRequest = getEffectiveRESTRequest( - tab.value.document.request, - { - name: "Env", - variables: combineEnvVariables(envs), - } - ) +): [ + () => void, + Promise< + | E.Left<"script_fail" | "cancellation"> + | E.Right> + > +] { + let cancelCalled = false + let cancelFunc: (() => void) | null = null - const stream = createRESTNetworkRequestStream(effectiveRequest) + const cancel = () => { + cancelCalled = true + cancelFunc?.() + } - // Run Test Script when request ran successfully - const subscription = stream - .pipe(filter((res) => res.type === "success" || res.type === "fail")) - .subscribe(async (res) => { - if (res.type === "success" || res.type === "fail") { - executedResponses$.next( - // @ts-expect-error Typescript can't figure out this inference for some reason - res - ) + const res = getFinalEnvsFromPreRequest( + tab.value.document.request.preRequestScript, + getCombinedEnvVariables() + )().then((envs) => { + if (cancelCalled) return E.left("cancellation" as const) - const runResult = await runTestScript(res.req.testScript, envs, { + if (E.isLeft(envs)) { + console.error(envs.left) + return E.left("script_fail" as const) + } + + const effectiveRequest = getEffectiveRESTRequest( + tab.value.document.request, + { + name: "Env", + variables: combineEnvVariables(envs.right), + } + ) + + const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest) + cancelFunc = cancelRun + + const subscription = stream + .pipe(filter((res) => res.type === "success" || res.type === "fail")) + .subscribe(async (res) => { + if (res.type === "success" || res.type === "fail") { + executedResponses$.next( + // @ts-expect-error Typescript can't figure out this inference for some reason + res + ) + + const runResult = await runTestScript( + res.req.testScript, + envs.right, + { status: res.statusCode, body: getTestableBody(res), headers: res.headers, - })() - - if (isRight(runResult)) { - tab.value.testResults = translateToSandboxTestResults( - runResult.right - ) - - setGlobalEnvVariables(runResult.right.envs.global) - - if ( - environmentsStore.value.selectedEnvironmentIndex.type === - "MY_ENV" - ) { - const env = getEnvironment({ - type: "MY_ENV", - index: environmentsStore.value.selectedEnvironmentIndex.index, - }) - updateEnvironment( - environmentsStore.value.selectedEnvironmentIndex.index, - { - ...env, - variables: runResult.right.envs.selected, - } - ) - } else if ( - environmentsStore.value.selectedEnvironmentIndex.type === - "TEAM_ENV" - ) { - const env = getEnvironment({ - type: "TEAM_ENV", - }) - pipe( - updateTeamEnvironment( - JSON.stringify(runResult.right.envs.selected), - environmentsStore.value.selectedEnvironmentIndex.teamEnvID, - env.name - ) - )() - } - } else { - tab.value.testResults = { - description: "", - expectResults: [], - tests: [], - envDiff: { - global: { - additions: [], - deletions: [], - updations: [], - }, - selected: { - additions: [], - deletions: [], - updations: [], - }, - }, - scriptError: true, - } } + )() - subscription.unsubscribe() + if (E.isRight(runResult)) { + tab.value.testResults = translateToSandboxTestResults( + runResult.right + ) + + setGlobalEnvVariables(runResult.right.envs.global) + + if ( + environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV" + ) { + const env = getEnvironment({ + type: "MY_ENV", + index: environmentsStore.value.selectedEnvironmentIndex.index, + }) + updateEnvironment( + environmentsStore.value.selectedEnvironmentIndex.index, + { + ...env, + variables: runResult.right.envs.selected, + } + ) + } else if ( + environmentsStore.value.selectedEnvironmentIndex.type === + "TEAM_ENV" + ) { + const env = getEnvironment({ + type: "TEAM_ENV", + }) + pipe( + updateTeamEnvironment( + JSON.stringify(runResult.right.envs.selected), + environmentsStore.value.selectedEnvironmentIndex.teamEnvID, + env.name + ) + )() + } + } else { + tab.value.testResults = { + description: "", + expectResults: [], + tests: [], + envDiff: { + global: { + additions: [], + deletions: [], + updations: [], + }, + selected: { + additions: [], + deletions: [], + updations: [], + }, + }, + scriptError: true, + } } - }) - return TE.right(stream) - }) - ) + subscription.unsubscribe() + } + }) + + return E.right(stream) + }) + + return [cancel, res] +} const getAddedEnvVariables = ( current: Environment["variables"], diff --git a/packages/hoppscotch-common/src/helpers/network.ts b/packages/hoppscotch-common/src/helpers/network.ts index 4e2f2522f..33d2f8d2f 100644 --- a/packages/hoppscotch-common/src/helpers/network.ts +++ b/packages/hoppscotch-common/src/helpers/network.ts @@ -1,189 +1,107 @@ -import { AxiosResponse, AxiosRequestConfig } from "axios" +import { AxiosRequestConfig } from "axios" import { BehaviorSubject, Observable } from "rxjs" import { cloneDeep } from "lodash-es" -import * as T from "fp-ts/Task" +import * as E from "fp-ts/Either" import * as TE from "fp-ts/TaskEither" -import { pipe } from "fp-ts/function" -import AxiosStrategy, { - cancelRunningAxiosRequest, -} from "./strategies/AxiosStrategy" -import ExtensionStrategy, { - cancelRunningExtensionRequest, - hasExtensionInstalled, -} from "./strategies/ExtensionStrategy" import { HoppRESTResponse } from "./types/HoppRESTResponse" import { EffectiveHoppRESTRequest } from "./utils/EffectiveURL" -import { settingsStore } from "~/newstore/settings" - -export type NetworkResponse = AxiosResponse & { - config?: { - timeData?: { - startTime: number - endTime: number - } - } -} +import { getService } from "~/modules/dioc" +import { + InterceptorService, + NetworkResponse, +} from "~/services/interceptor.service" export type NetworkStrategy = ( req: AxiosRequestConfig ) => TE.TaskEither export const cancelRunningRequest = () => { - if (isExtensionsAllowed() && hasExtensionInstalled()) { - cancelRunningExtensionRequest() - } else { - cancelRunningAxiosRequest() - } + // TODO: Implement } -const isExtensionsAllowed = () => settingsStore.value.EXTENSIONS_ENABLED - -const runAppropriateStrategy = (req: AxiosRequestConfig) => { - if (isExtensionsAllowed() && hasExtensionInstalled()) { - return ExtensionStrategy(req) - } - - return AxiosStrategy(req) -} - -/** - * Returns an identifier for how a request will be ran - * if the system is asked to fire a request - * - */ -export function getCurrentStrategyID() { - if (isExtensionsAllowed() && hasExtensionInstalled()) { - return "extension" as const - } else if (settingsStore.value.PROXY_ENABLED) { - return "proxy" as const - } else { - return "normal" as const - } -} - -export const sendNetworkRequest = (req: any) => - pipe( - runAppropriateStrategy(req), - TE.getOrElse((e) => { - throw e - }) - )() - -const processResponse = ( +function processResponse( res: NetworkResponse, req: EffectiveHoppRESTRequest, backupTimeStart: number, backupTimeEnd: number, successState: HoppRESTResponse["type"] -) => - pipe( - TE.Do, +) { + const contentLength = res.headers["content-length"] + ? parseInt(res.headers["content-length"]) + : (res.data as ArrayBuffer).byteLength - // Calculate the content length - TE.bind("contentLength", () => - TE.of( - res.headers["content-length"] - ? parseInt(res.headers["content-length"]) - : (res.data as ArrayBuffer).byteLength - ) - ), - - // Building the final response object - TE.map( - ({ contentLength }) => - { - type: successState, - statusCode: res.status, - body: res.data, - headers: Object.keys(res.headers).map((x) => ({ - key: x, - value: res.headers[x], - })), - meta: { - responseSize: contentLength, - responseDuration: backupTimeEnd - backupTimeStart, - }, - req, - } - ) - ) + return { + type: successState, + statusCode: res.status, + body: res.data, + headers: Object.keys(res.headers).map((x) => ({ + key: x, + value: res.headers[x], + })), + meta: { + responseSize: contentLength, + responseDuration: backupTimeEnd - backupTimeStart, + }, + req, + } +} export function createRESTNetworkRequestStream( request: EffectiveHoppRESTRequest -): Observable { +): [Observable, () => void] { const response = new BehaviorSubject({ type: "loading", req: request, }) - pipe( - TE.Do, + const req = cloneDeep(request) - // Get a deep clone of the request - TE.bind("req", () => TE.of(cloneDeep(request))), + const headers = req.effectiveFinalHeaders.reduce((acc, { key, value }) => { + return Object.assign(acc, { [key]: value }) + }, {}) - // Assembling headers object - TE.bind("headers", ({ req }) => - TE.of( - req.effectiveFinalHeaders.reduce((acc, { key, value }) => { - return Object.assign(acc, { [key]: value }) - }, {}) + const params = new URLSearchParams() + for (const param of req.effectiveFinalParams) { + params.append(param.key, param.value) + } + + const backupTimeStart = Date.now() + + const service = getService(InterceptorService) + + const res = service.runRequest({ + method: req.method as any, + url: req.effectiveFinalURL.trim(), + headers, + params, + data: req.effectiveFinalBody, + }) + + res.response.then((res) => { + const backupTimeEnd = Date.now() + + if (E.isRight(res)) { + const processedRes = processResponse( + res.right, + req, + backupTimeStart, + backupTimeEnd, + "success" ) - ), - // Assembling params object - TE.bind("params", ({ req }) => { - const params = new URLSearchParams() - req.effectiveFinalParams.forEach((x) => { - params.append(x.key, x.value) - }) - return TE.of(params) - }), - - // Keeping the backup start time - TE.bind("backupTimeStart", () => TE.of(Date.now())), - - // Running the request and getting the response - TE.bind("res", ({ req, headers, params }) => - runAppropriateStrategy({ - method: req.method as any, - url: req.effectiveFinalURL.trim(), - headers, - params, - data: req.effectiveFinalBody, - }) - ), - - // Getting the backup end time - TE.bind("backupTimeEnd", () => TE.of(Date.now())), - - // Assemble the final response object - TE.chainW(({ req, res, backupTimeEnd, backupTimeStart }) => - processResponse(res, req, backupTimeStart, backupTimeEnd, "success") - ), - - // Writing success state to the stream - TE.chain((res) => { - response.next(res) + response.next(processedRes) response.complete() - return TE.of(res) - }), + return + } - // Package the error type - TE.getOrElseW((e) => { - const obj: HoppRESTResponse = { - type: "network_fail", - error: e, - req: request, - } - - response.next(obj) - response.complete() - - return T.of(obj) + response.next({ + type: "network_fail", + req, + error: res.left, }) - )() + response.complete() + }) - return response + return [response, () => res.cancel()] } diff --git a/packages/hoppscotch-common/src/helpers/strategies/AxiosStrategy.ts b/packages/hoppscotch-common/src/helpers/strategies/AxiosStrategy.ts deleted file mode 100644 index 137bc6592..000000000 --- a/packages/hoppscotch-common/src/helpers/strategies/AxiosStrategy.ts +++ /dev/null @@ -1,163 +0,0 @@ -import axios, { AxiosRequestConfig } from "axios" -import { v4 } from "uuid" -import { pipe } from "fp-ts/function" -import * as TE from "fp-ts/TaskEither" -import { cloneDeep } from "lodash-es" -import { NetworkResponse, NetworkStrategy } from "../network" -import { decodeB64StringToArrayBuffer } from "../utils/b64" -import { settingsStore } from "~/newstore/settings" - -let cancelSource = axios.CancelToken.source() - -type ProxyHeaders = { - "multipart-part-key"?: string -} - -type ProxyPayloadType = FormData | (AxiosRequestConfig & { wantsBinary: true }) - -export const cancelRunningAxiosRequest = () => { - cancelSource.cancel() - - // Create a new cancel token - cancelSource = axios.CancelToken.source() -} - -const getProxyPayload = ( - req: AxiosRequestConfig, - multipartKey: string | null -) => { - let payload: ProxyPayloadType = { - ...req, - wantsBinary: true, - accessToken: import.meta.env.VITE_PROXYSCOTCH_ACCESS_TOKEN ?? "", - } - - if (payload.data instanceof FormData) { - const formData = payload.data - payload.data = "" - formData.append(multipartKey!, JSON.stringify(payload)) - payload = formData - } - - return payload -} - -const preProcessRequest = (req: AxiosRequestConfig): AxiosRequestConfig => { - const reqClone = cloneDeep(req) - - // If the parameters are URLSearchParams, inject them to URL instead - // This prevents issues of marshalling the URLSearchParams to the proxy - if (reqClone.params instanceof URLSearchParams) { - try { - const url = new URL(reqClone.url ?? "") - - for (const [key, value] of reqClone.params.entries()) { - url.searchParams.append(key, value) - } - - reqClone.url = url.toString() - } catch (e) { - // making this a non-empty block, so we can make the linter happy. - // we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :) - } - - reqClone.params = {} - } - - return reqClone -} - -const axiosWithProxy: NetworkStrategy = (req) => - pipe( - TE.Do, - - TE.bind("processedReq", () => TE.of(preProcessRequest(req))), - - // If the request has FormData, the proxy needs a key - TE.bind("multipartKey", ({ processedReq }) => - TE.of( - processedReq.data instanceof FormData - ? `proxyRequestData-${v4()}` - : null - ) - ), - - // Build headers to send - TE.bind("headers", ({ processedReq, multipartKey }) => - TE.of( - processedReq.data instanceof FormData - ? { - "multipart-part-key": multipartKey, - } - : {} - ) - ), - - // Create payload - TE.bind("payload", ({ processedReq, multipartKey }) => - TE.of(getProxyPayload(processedReq, multipartKey)) - ), - - // Run the proxy request - TE.chain(({ payload, headers }) => - TE.tryCatch( - () => - axios.post( - settingsStore.value.PROXY_URL || "https://proxy.hoppscotch.io", - payload, - { - headers, - cancelToken: cancelSource.token, - } - ), - (reason) => - axios.isCancel(reason) - ? "cancellation" // Convert cancellation errors into cancellation strings - : reason - ) - ), - - // Check success predicate - TE.chain( - TE.fromPredicate( - ({ data }) => data.success, - ({ data }) => data.data.message || "Proxy Error" - ) - ), - - // Process Base64 - TE.chain(({ data }) => { - if (data.isBinary) { - data.data = decodeB64StringToArrayBuffer(data.data) - } - - return TE.of(data) - }) - ) - -const axiosWithoutProxy: NetworkStrategy = (req) => - pipe( - TE.tryCatch( - () => - axios({ - ...req, - cancelToken: (cancelSource && cancelSource.token) || "", - responseType: "arraybuffer", - }), - (e) => (axios.isCancel(e) ? "cancellation" : (e as any)) - ), - - TE.orElse((e) => - e !== "cancellation" && e.response - ? TE.right(e.response as NetworkResponse) - : TE.left(e) - ) - ) - -const axiosStrategy: NetworkStrategy = (req) => - pipe( - req, - settingsStore.value.PROXY_ENABLED ? axiosWithProxy : axiosWithoutProxy - ) - -export default axiosStrategy diff --git a/packages/hoppscotch-common/src/helpers/strategies/ExtensionStrategy.ts b/packages/hoppscotch-common/src/helpers/strategies/ExtensionStrategy.ts deleted file mode 100644 index 5bb0b69df..000000000 --- a/packages/hoppscotch-common/src/helpers/strategies/ExtensionStrategy.ts +++ /dev/null @@ -1,128 +0,0 @@ -import * as TE from "fp-ts/TaskEither" -import * as O from "fp-ts/Option" -import { pipe } from "fp-ts/function" -import { AxiosRequestConfig } from "axios" -import { cloneDeep } from "lodash-es" -import { NetworkResponse, NetworkStrategy } from "../network" -import { browserIsChrome, browserIsFirefox } from "../utils/userAgent" - -export const hasExtensionInstalled = () => - typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined" - -export const hasChromeExtensionInstalled = () => - hasExtensionInstalled() && browserIsChrome() - -export const hasFirefoxExtensionInstalled = () => - hasExtensionInstalled() && browserIsFirefox() - -export const cancelRunningExtensionRequest = () => { - window.__POSTWOMAN_EXTENSION_HOOK__?.cancelRequest() -} - -export const defineSubscribableObject = (obj: T) => { - const proxyObject = { - ...obj, - _subscribers: {} as { - // eslint-disable-next-line no-unused-vars - [key in keyof T]?: ((...args: any[]) => any)[] - }, - subscribe(prop: keyof T, func: (...args: any[]) => any): void { - if (Array.isArray(this._subscribers[prop])) { - this._subscribers[prop]?.push(func) - } else { - this._subscribers[prop] = [func] - } - }, - } - - type SubscribableProxyObject = typeof proxyObject - - return new Proxy(proxyObject, { - set(obj, prop, newVal) { - obj[prop as keyof SubscribableProxyObject] = newVal - - const currentSubscribers = obj._subscribers[prop as keyof T] - - if (Array.isArray(currentSubscribers)) { - for (const subscriber of currentSubscribers) { - subscriber(newVal) - } - } - - return true - }, - }) -} - -const preProcessRequest = (req: AxiosRequestConfig): AxiosRequestConfig => { - const reqClone = cloneDeep(req) - - // If the parameters are URLSearchParams, inject them to URL instead - // This prevents marshalling issues with structured cloning of URLSearchParams - if (reqClone.params instanceof URLSearchParams) { - try { - const url = new URL(reqClone.url ?? "") - - for (const [key, value] of reqClone.params.entries()) { - url.searchParams.append(key, value) - } - - reqClone.url = url.toString() - } catch (e) { - // making this a non-empty block, so we can make the linter happy. - // we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :) - } - - reqClone.params = {} - } - - return reqClone -} - -const extensionStrategy: NetworkStrategy = (req) => - pipe( - TE.Do, - - TE.bind("processedReq", () => TE.of(preProcessRequest(req))), - - // Storeing backup timing data in case the extension does not have that info - TE.bind("backupTimeDataStart", () => TE.of(new Date().getTime())), - - // Run the request - TE.bind("response", ({ processedReq }) => - pipe( - window.__POSTWOMAN_EXTENSION_HOOK__, - O.fromNullable, - TE.fromOption(() => "NO_PW_EXT_HOOK" as const), - TE.chain((extensionHook) => - TE.tryCatch( - () => - extensionHook.sendRequest({ - ...processedReq, - wantsBinary: true, - }), - (err) => err as any - ) - ) - ) - ), - - // Inject backup time data if not present - TE.map(({ backupTimeDataStart, response }) => ({ - ...response, - config: { - timeData: { - startTime: backupTimeDataStart, - endTime: new Date().getTime(), - }, - ...response.config, - }, - })), - TE.orElse((e) => - e !== "cancellation" && e.response - ? TE.right(e.response as NetworkResponse) - : TE.left(e) - ) - ) - -export default extensionStrategy diff --git a/packages/hoppscotch-common/src/helpers/strategies/__tests__/AxiosStrategy-NoProxy.spec.js b/packages/hoppscotch-common/src/helpers/strategies/__tests__/AxiosStrategy-NoProxy.spec.js deleted file mode 100644 index 6005eaf13..000000000 --- a/packages/hoppscotch-common/src/helpers/strategies/__tests__/AxiosStrategy-NoProxy.spec.js +++ /dev/null @@ -1,76 +0,0 @@ -import { vi, describe, expect, test } from "vitest" -import axios from "axios" -import axiosStrategy from "../AxiosStrategy" - -vi.mock("axios") -vi.mock("~/newstore/settings", () => { - return { - __esModule: true, - settingsStore: { - value: { - PROXY_ENABLED: false, - }, - }, - } -}) - -axios.CancelToken.source.mockReturnValue({ token: "test" }) -axios.mockResolvedValue({}) - -describe("axiosStrategy", () => { - describe("No-Proxy Requests", () => { - test("sends request to the actual sender if proxy disabled", async () => { - await axiosStrategy({ url: "test" })() - - expect(axios).toBeCalledWith( - expect.objectContaining({ - url: "test", - }) - ) - }) - - test("asks axios to return data as arraybuffer", async () => { - await axiosStrategy({ url: "test" })() - - expect(axios).toBeCalledWith( - expect.objectContaining({ - responseType: "arraybuffer", - }) - ) - }) - - test("resolves successful requests", async () => { - expect(await axiosStrategy({})()).toBeRight() - }) - - test("rejects cancel errors with text 'cancellation'", async () => { - axios.isCancel.mockReturnValueOnce(true) - axios.mockRejectedValue("err") - - expect(await axiosStrategy({})()).toEqualLeft("cancellation") - }) - - test("rejects non-cancellation errors as-is", async () => { - axios.isCancel.mockReturnValueOnce(false) - axios.mockRejectedValue("err") - - expect(await axiosStrategy({})()).toEqualLeft("err") - }) - - test("non-cancellation errors that have response data are right", async () => { - const errorResponse = { error: "errr" } - axios.isCancel.mockReturnValueOnce(false) - axios.mockRejectedValue({ - response: { - data: Buffer.from(JSON.stringify(errorResponse), "utf8").toString( - "base64" - ), - }, - }) - - expect(await axiosStrategy({})()).toSubsetEqualRight({ - data: "eyJlcnJvciI6ImVycnIifQ==", - }) - }) - }) -}) diff --git a/packages/hoppscotch-common/src/helpers/strategies/__tests__/AxiosStrategy-Proxy.spec.js b/packages/hoppscotch-common/src/helpers/strategies/__tests__/AxiosStrategy-Proxy.spec.js deleted file mode 100644 index 314cac603..000000000 --- a/packages/hoppscotch-common/src/helpers/strategies/__tests__/AxiosStrategy-Proxy.spec.js +++ /dev/null @@ -1,145 +0,0 @@ -import { describe, test, expect, vi } from "vitest" -import axios from "axios" -import axiosStrategy from "../AxiosStrategy" - -vi.mock("../../utils/b64", () => ({ - __esModule: true, - decodeB64StringToArrayBuffer: vi.fn((data) => `${data}-converted`), -})) -vi.mock("~/newstore/settings", () => { - return { - __esModule: true, - settingsStore: { - value: { - PROXY_ENABLED: true, - PROXY_URL: "test", - }, - }, - } -}) - -describe("axiosStrategy", () => { - describe("Proxy Requests", () => { - test("sends POST request to proxy if proxy is enabled", async () => { - let passedURL - - vi.spyOn(axios, "post").mockImplementation((url) => { - passedURL = url - return Promise.resolve({ data: { success: true, isBinary: false } }) - }) - - await axiosStrategy({})() - - expect(passedURL).toEqual("test") - }) - - test("passes request fields to axios properly", async () => { - const reqFields = { - testA: "testA", - testB: "testB", - testC: "testC", - } - - let passedFields - - vi.spyOn(axios, "post").mockImplementation((_url, req) => { - passedFields = req - return Promise.resolve({ data: { success: true, isBinary: false } }) - }) - - await axiosStrategy(reqFields)() - - expect(passedFields).toMatchObject(reqFields) - }) - - test("passes wantsBinary field", async () => { - let passedFields - - vi.spyOn(axios, "post").mockImplementation((_url, req) => { - passedFields = req - return Promise.resolve({ data: { success: true, isBinary: false } }) - }) - - await axiosStrategy({})() - - expect(passedFields).toHaveProperty("wantsBinary") - }) - - test("checks for proxy response success field and throws error message for non-success", async () => { - vi.spyOn(axios, "post").mockResolvedValue({ - data: { - success: false, - data: { - message: "test message", - }, - }, - }) - - expect(await axiosStrategy({})()).toEqualLeft("test message") - }) - - test("checks for proxy response success field and throws error 'Proxy Error' for non-success", async () => { - vi.spyOn(axios, "post").mockResolvedValue({ - data: { - success: false, - data: {}, - }, - }) - - expect(await axiosStrategy({})()).toBeLeft("Proxy Error") - }) - - test("checks for proxy response success and doesn't left for success", async () => { - vi.spyOn(axios, "post").mockResolvedValue({ - data: { - success: true, - data: {}, - }, - }) - - expect(await axiosStrategy({})()).toBeRight() - }) - - test("checks isBinary response field and right with the converted value if so", async () => { - vi.spyOn(axios, "post").mockResolvedValue({ - data: { - success: true, - isBinary: true, - data: "testdata", - }, - }) - - expect(await axiosStrategy({})()).toSubsetEqualRight({ - data: "testdata-converted", - }) - }) - - test("checks isBinary response field and right with the actual value if not so", async () => { - vi.spyOn(axios, "post").mockResolvedValue({ - data: { - success: true, - isBinary: false, - data: "testdata", - }, - }) - - expect(await axiosStrategy({})()).toSubsetEqualRight({ - data: "testdata", - }) - }) - - test("cancel errors are returned a left with the string 'cancellation'", async () => { - vi.spyOn(axios, "post").mockRejectedValue("errr") - vi.spyOn(axios, "isCancel").mockReturnValueOnce(true) - - expect(await axiosStrategy({})()).toEqualLeft("cancellation") - }) - - test("non-cancellation errors return a left", async () => { - vi.spyOn(axios, "post").mockRejectedValue("errr") - vi.spyOn(axios, "isCancel").mockReturnValueOnce(false) - - expect(await axiosStrategy({})()).toEqualLeft("errr") - }) - }) -}) diff --git a/packages/hoppscotch-common/src/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js b/packages/hoppscotch-common/src/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js deleted file mode 100644 index 99d977373..000000000 --- a/packages/hoppscotch-common/src/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js +++ /dev/null @@ -1,212 +0,0 @@ -import { vi, describe, expect, test, beforeEach } from "vitest" -import extensionStrategy, { - hasExtensionInstalled, - hasChromeExtensionInstalled, - hasFirefoxExtensionInstalled, - cancelRunningExtensionRequest, -} from "../ExtensionStrategy" - -vi.mock("../../utils/b64", () => ({ - __esModule: true, - decodeB64StringToArrayBuffer: vi.fn((data) => `${data}-converted`), -})) - -vi.mock("~/newstore/settings", () => { - return { - __esModule: true, - settingsStore: { - value: { - EXTENSIONS_ENABLED: true, - PROXY_ENABLED: false, - }, - }, - } -}) - -describe("hasExtensionInstalled", () => { - test("returns true if extension is present and hooked", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = {} - - expect(hasExtensionInstalled()).toEqual(true) - }) - - test("returns false if extension not present or not hooked", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = undefined - - expect(hasExtensionInstalled()).toEqual(false) - }) -}) - -describe("hasChromeExtensionInstalled", () => { - test("returns true if extension is hooked and browser is chrome", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = {} - vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome") - vi.spyOn(navigator, "vendor", "get").mockReturnValue("Google") - - expect(hasChromeExtensionInstalled()).toEqual(true) - }) - - test("returns false if extension is hooked and browser is not chrome", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = {} - vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox") - vi.spyOn(navigator, "vendor", "get").mockReturnValue("Google") - - expect(hasChromeExtensionInstalled()).toEqual(false) - }) - - test("returns false if extension not installed and browser is chrome", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = undefined - vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome") - vi.spyOn(navigator, "vendor", "get").mockReturnValue("Google") - - expect(hasChromeExtensionInstalled()).toEqual(false) - }) - - test("returns false if extension not installed and browser is not chrome", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = undefined - vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox") - vi.spyOn(navigator, "vendor", "get").mockReturnValue("Google") - - expect(hasChromeExtensionInstalled()).toEqual(false) - }) -}) - -describe("hasFirefoxExtensionInstalled", () => { - test("returns true if extension is hooked and browser is firefox", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = {} - vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox") - - expect(hasFirefoxExtensionInstalled()).toEqual(true) - }) - - test("returns false if extension is hooked and browser is not firefox", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = {} - vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome") - - expect(hasFirefoxExtensionInstalled()).toEqual(false) - }) - - test("returns false if extension not installed and browser is firefox", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = undefined - vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox") - - expect(hasFirefoxExtensionInstalled()).toEqual(false) - }) - - test("returns false if extension not installed and browser is not firefox", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = undefined - vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome") - - expect(hasFirefoxExtensionInstalled()).toEqual(false) - }) -}) - -describe("cancelRunningExtensionRequest", () => { - const cancelFunc = vi.fn() - - beforeEach(() => { - cancelFunc.mockClear() - }) - - test("cancels request if extension installed and function present in hook", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = { - cancelRequest: cancelFunc, - } - - cancelRunningExtensionRequest() - expect(cancelFunc).toHaveBeenCalledTimes(1) - }) - - test("does not cancel request if extension not installed", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = undefined - - cancelRunningExtensionRequest() - expect(cancelFunc).not.toHaveBeenCalled() - }) -}) - -describe("extensionStrategy", () => { - const sendReqFunc = vi.fn() - - beforeEach(() => { - sendReqFunc.mockClear() - }) - - describe("Non-Proxy Requests", () => { - test("ask extension to send request", async () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = { - sendRequest: sendReqFunc, - } - - sendReqFunc.mockResolvedValue({ - data: '{"success":true,"data":""}', - }) - - await extensionStrategy({})() - - expect(sendReqFunc).toHaveBeenCalledTimes(1) - }) - - test("sends request to the actual sender if proxy disabled", async () => { - let passedUrl - - global.__POSTWOMAN_EXTENSION_HOOK__ = { - sendRequest: sendReqFunc, - } - - sendReqFunc.mockImplementation(({ url }) => { - passedUrl = url - - return Promise.resolve({ - data: '{"success":true,"data":""}', - }) - }) - - await extensionStrategy({ url: "test" })() - - expect(passedUrl).toEqual("test") - }) - - test("asks extension to get binary data", async () => { - let passedFields - - global.__POSTWOMAN_EXTENSION_HOOK__ = { - sendRequest: sendReqFunc, - } - - sendReqFunc.mockImplementation((fields) => { - passedFields = fields - - return Promise.resolve({ - data: '{"success":true,"data":""}', - }) - }) - - await extensionStrategy({})() - - expect(passedFields).toHaveProperty("wantsBinary") - }) - - test("rights successful requests", async () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = { - sendRequest: sendReqFunc, - } - - sendReqFunc.mockResolvedValue({ - data: '{"success":true,"data":""}', - }) - - expect(await extensionStrategy({})()).toBeRight() - }) - - test("rejects errors as-is", async () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = { - sendRequest: sendReqFunc, - } - - sendReqFunc.mockRejectedValue("err") - - expect(await extensionStrategy({})()).toEqualLeft("err") - }) - }) -}) diff --git a/packages/hoppscotch-common/src/helpers/types/HoppRESTResponse.ts b/packages/hoppscotch-common/src/helpers/types/HoppRESTResponse.ts index b43f6f6fd..bb5141d4c 100644 --- a/packages/hoppscotch-common/src/helpers/types/HoppRESTResponse.ts +++ b/packages/hoppscotch-common/src/helpers/types/HoppRESTResponse.ts @@ -19,7 +19,7 @@ export type HoppRESTResponse = } | { type: "network_fail" - error: Error + error: unknown req: HoppRESTRequest } diff --git a/packages/hoppscotch-common/src/modules/hoppExtension.ts b/packages/hoppscotch-common/src/modules/hoppExtension.ts deleted file mode 100644 index b4e114472..000000000 --- a/packages/hoppscotch-common/src/modules/hoppExtension.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { HoppModule } from "." -import { - changeExtensionStatus, - ExtensionStatus, -} from "~/newstore/HoppExtension" -import { ref } from "vue" -import { defineSubscribableObject } from "~/helpers/strategies/ExtensionStrategy" - -/* Module defining the hooking mechanism between Hoppscotch and the Hoppscotch Browser Extension */ - -export default { - onVueAppInit() { - const extensionPollIntervalId = ref>() - - if (window.__HOPP_EXTENSION_STATUS_PROXY__) { - changeExtensionStatus(window.__HOPP_EXTENSION_STATUS_PROXY__.status) - - window.__HOPP_EXTENSION_STATUS_PROXY__.subscribe( - "status", - (status: ExtensionStatus) => changeExtensionStatus(status) - ) - } else { - const statusProxy = defineSubscribableObject({ - status: "waiting" as ExtensionStatus, - }) - - window.__HOPP_EXTENSION_STATUS_PROXY__ = statusProxy - statusProxy.subscribe("status", (status: ExtensionStatus) => - changeExtensionStatus(status) - ) - - /** - * Keeping identifying extension backward compatible - * We are assuming the default version is 0.24 or later. So if the extension exists, its identified immediately, - * then we use a poll to find the version, this will get the version for 0.24 and any other version - * of the extension, but will have a slight lag. - * 0.24 users will get the benefits of 0.24, while the extension won't break for the old users - */ - extensionPollIntervalId.value = setInterval(() => { - if (typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined") { - if (extensionPollIntervalId.value) - clearInterval(extensionPollIntervalId.value) - - const version = window.__POSTWOMAN_EXTENSION_HOOK__.getVersion() - - // When the version is not 0.24 or higher, the extension wont do this. so we have to do it manually - if ( - version.major === 0 && - version.minor <= 23 && - window.__HOPP_EXTENSION_STATUS_PROXY__ - ) { - window.__HOPP_EXTENSION_STATUS_PROXY__.status = "available" - } - } - }, 2000) - } - }, -} diff --git a/packages/hoppscotch-common/src/modules/interceptors.ts b/packages/hoppscotch-common/src/modules/interceptors.ts new file mode 100644 index 000000000..43d71a322 --- /dev/null +++ b/packages/hoppscotch-common/src/modules/interceptors.ts @@ -0,0 +1,46 @@ +import { InterceptorService } from "~/services/interceptor.service" +import { HoppModule } from "." +import { getService } from "./dioc" +import { platform } from "~/platform" +import { watch } from "vue" +import { applySetting } from "~/newstore/settings" +import { useSettingStatic } from "~/composables/settings" + +export default { + onVueAppInit() { + const interceptorService = getService(InterceptorService) + + for (const interceptorDef of platform.interceptors.interceptors) { + if (interceptorDef.type === "standalone") { + interceptorService.registerInterceptor(interceptorDef.interceptor) + } else { + const service = getService(interceptorDef.service) + + interceptorService.registerInterceptor(service) + } + } + + interceptorService.currentInterceptorID.value = + platform.interceptors.default + + watch(interceptorService.currentInterceptorID, (id) => { + applySetting( + "CURRENT_INTERCEPTOR_ID", + id ?? platform.interceptors.default + ) + }) + + const [setting] = useSettingStatic("CURRENT_INTERCEPTOR_ID") + + watch( + setting, + () => { + interceptorService.currentInterceptorID.value = + setting.value ?? platform.interceptors.default + }, + { + immediate: true, + } + ) + }, +} diff --git a/packages/hoppscotch-common/src/newstore/localpersistence.ts b/packages/hoppscotch-common/src/newstore/localpersistence.ts index c67776b4d..b7b94c3e4 100644 --- a/packages/hoppscotch-common/src/newstore/localpersistence.ts +++ b/packages/hoppscotch-common/src/newstore/localpersistence.ts @@ -9,10 +9,11 @@ import { import { settingsStore, bulkApplySettings, - defaultSettings, + getDefaultSettings, applySetting, HoppAccentColor, HoppBgColor, + performSettingsDataMigrations, } from "./settings" import { restHistoryStore, @@ -80,7 +81,7 @@ function checkAndMigrateOldSettings() { const { postwoman } = vuexData if (!isEmpty(postwoman?.settings)) { - const settingsData = assign(clone(defaultSettings), postwoman.settings) + const settingsData = assign(clone(getDefaultSettings()), postwoman.settings) window.localStorage.setItem("settings", JSON.stringify(settingsData)) @@ -150,8 +151,12 @@ function setupSettingsPersistence() { window.localStorage.getItem("settings") || "{}" ) - if (settingsData) { - bulkApplySettings(settingsData) + const updatedSettings = settingsData + ? performSettingsDataMigrations(settingsData) + : settingsData + + if (updatedSettings) { + bulkApplySettings(updatedSettings) } settingsStore.subject$.subscribe((settings) => { diff --git a/packages/hoppscotch-common/src/newstore/settings.ts b/packages/hoppscotch-common/src/newstore/settings.ts index cc4ffa43f..c23d57b75 100644 --- a/packages/hoppscotch-common/src/newstore/settings.ts +++ b/packages/hoppscotch-common/src/newstore/settings.ts @@ -1,5 +1,5 @@ import { pluck, distinctUntilChanged } from "rxjs/operators" -import { has } from "lodash-es" +import { cloneDeep, defaultsDeep, has } from "lodash-es" import { Observable } from "rxjs" import DispatchingStore, { defineDispatchers } from "./DispatchingStore" @@ -32,9 +32,10 @@ export type SettingsDef = { syncHistory: boolean syncEnvironments: boolean - PROXY_ENABLED: boolean PROXY_URL: string - EXTENSIONS_ENABLED: boolean + + CURRENT_INTERCEPTOR_ID: string + URL_EXCLUDES: { auth: boolean httpUser: boolean @@ -53,14 +54,15 @@ export type SettingsDef = { COLUMN_LAYOUT: boolean } -export const defaultSettings: SettingsDef = { +export const getDefaultSettings = (): SettingsDef => ({ syncCollections: true, syncHistory: true, syncEnvironments: true, - PROXY_ENABLED: false, + CURRENT_INTERCEPTOR_ID: "browser", // TODO: Allow the platform definition to take this place + + // TODO: Interceptor related settings should move under the interceptor systems PROXY_URL: "https://proxy.hoppscotch.io/", - EXTENSIONS_ENABLED: false, URL_EXCLUDES: { auth: true, httpUser: true, @@ -77,7 +79,7 @@ export const defaultSettings: SettingsDef = { ZEN_MODE: false, FONT_SIZE: "small", COLUMN_LAYOUT: true, -} +}) type ApplySettingPayload = { [K in keyof SettingsDef]: { @@ -86,8 +88,6 @@ type ApplySettingPayload = { } }[keyof SettingsDef] -const validKeys = Object.keys(defaultSettings) - const dispatchers = defineDispatchers({ bulkApplySettings(_currentState: SettingsDef, payload: Partial) { return payload @@ -112,13 +112,6 @@ const dispatchers = defineDispatchers({ _currentState: SettingsDef, { settingKey, value }: ApplySettingPayload ) { - if (!validKeys.includes(settingKey)) { - // console.log( - // `Ignoring non-existent setting key '${settingKey}' assignment` - // ) - return {} - } - const result: Partial = { [settingKey]: value, } @@ -127,7 +120,10 @@ const dispatchers = defineDispatchers({ }, }) -export const settingsStore = new DispatchingStore(defaultSettings, dispatchers) +export const settingsStore = new DispatchingStore( + getDefaultSettings(), + dispatchers +) /** * An observable value to make avail all the state information at once @@ -156,16 +152,39 @@ export function toggleSetting(settingKey: KeysMatching) { }) } -export function applySetting( - settingKey: K["settingKey"], - value: K["value"] +export function applySetting( + settingKey: K, + value: SettingsDef[K] ) { settingsStore.dispatch({ dispatcher: "applySetting", - // @ts-expect-error TS is not able to understand the type semantics here payload: { + // @ts-expect-error TS is not able to understand the type semantics here settingKey, + // @ts-expect-error TS is not able to understand the type semantics here value, }, }) } + +export function performSettingsDataMigrations(data: any): SettingsDef { + const source = cloneDeep(data) + + if (source["EXTENSIONS_ENABLED"]) { + const result = JSON.parse(source["EXTENSIONS_ENABLED"]) + + if (result) source["CURRENT_INTERCEPTOR_ID"] = "extension" + delete source["EXTENSIONS_ENABLED"] + } + + if (source["PROXY_ENABLED"]) { + const result = JSON.parse(source["PROXY_ENABLED"]) + + if (result) source["CURRENT_INTERCEPTOR_ID"] = "proxy" + delete source["PROXY_ENABLED"] + } + + const final = defaultsDeep(source, getDefaultSettings()) + + return final +} diff --git a/packages/hoppscotch-common/src/pages/settings.vue b/packages/hoppscotch-common/src/pages/settings.vue index e4a0a991a..eac82068c 100644 --- a/packages/hoppscotch-common/src/pages/settings.vue +++ b/packages/hoppscotch-common/src/pages/settings.vue @@ -113,108 +113,11 @@

-
+

- {{ t("settings.extensions") }} + {{ settings.entryTitle(t) }}

-
- - {{ - `${t("settings.extension_version")}: v${ - extensionVersion.major - }.${extensionVersion.minor}` - }} - - - {{ t("settings.extension_version") }}: - {{ t("settings.extension_ver_not_reported") }} - -
-
- - - - - - -
-
-
- - {{ t("settings.extensions_use_toggle") }} - -
-
-
-
-

- {{ t("settings.proxy") }} -

-
- {{ - `${t("settings.official_proxy_hosting")} ${t( - "settings.read_the" - )}` - }} - . -
-
-
- - {{ t("settings.proxy_use_toggle") }} - -
-
-
- - - - -
+
@@ -236,62 +139,47 @@