diff --git a/packages/hoppscotch-app/components/http/Headers.vue b/packages/hoppscotch-app/components/http/Headers.vue index e25933640..651118c93 100644 --- a/packages/hoppscotch-app/components/http/Headers.vue +++ b/packages/hoppscotch-app/components/http/Headers.vue @@ -137,6 +137,47 @@ /> +
+ + + + + + + + + + + + +
diff --git a/packages/hoppscotch-app/components/http/RequestOptions.vue b/packages/hoppscotch-app/components/http/RequestOptions.vue index 486509733..3dec44d16 100644 --- a/packages/hoppscotch-app/components/http/RequestOptions.vue +++ b/packages/hoppscotch-app/components/http/RequestOptions.vue @@ -18,7 +18,7 @@ :label="`${$t('tab.headers')}`" :info="`${newActiveHeadersCount$}`" > - + diff --git a/packages/hoppscotch-app/components/smart/EnvInput.vue b/packages/hoppscotch-app/components/smart/EnvInput.vue index 196f0fafa..761cc4564 100644 --- a/packages/hoppscotch-app/components/smart/EnvInput.vue +++ b/packages/hoppscotch-app/components/smart/EnvInput.vue @@ -47,6 +47,7 @@ const props = withDefaults( styles: string envs: { key: string; value: string; source: string }[] | null focus: boolean + readonly: boolean }>(), { value: "", @@ -54,6 +55,7 @@ const props = withDefaults( styles: "", envs: null, focus: false, + readonly: false, } ) @@ -123,7 +125,23 @@ const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view) const initView = (el: any) => { const extensions: Extension = [ EditorView.contentAttributes.of({ "aria-label": props.placeholder }), + EditorView.updateListener.of((update) => { + if (props.readonly) { + update.view.contentDOM.inputMode = "none" + } + }), + EditorState.changeFilter.of(() => !props.readonly), inputTheme, + props.readonly + ? EditorView.theme({ + ".cm-content": { + caretColor: "var(--secondary-dark-color) !important", + color: "var(--secondary-dark-color) !important", + backgroundColor: "var(--divider-color) !important", + opacity: 0.25, + }, + }) + : EditorView.theme({}), tooltips({ position: "absolute", }), @@ -141,6 +159,8 @@ const initView = (el: any) => { ViewPlugin.fromClass( class { update(update: ViewUpdate) { + if (props.readonly) return + if (update.docChanged) { const prevValue = clone(cachedValue.value) diff --git a/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts b/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts index bcecc176c..fae74280e 100644 --- a/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts +++ b/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts @@ -11,6 +11,8 @@ import { parseBodyEnvVariables, parseRawKeyValueEntries, Environment, + HoppRESTHeader, + HoppRESTParam, } from "@hoppscotch/data" import { arrayFlatMap, arraySort } from "../functional/array" import { toFormData } from "../functional/formData" @@ -29,6 +31,146 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest { effectiveFinalBody: FormData | string | null } +/** + * Get headers that can be generated by authorization config of the request + * @param req Request to check + * @param envVars Currently active environment variables + * @returns The list of headers + */ +const getComputedAuthHeaders = ( + req: HoppRESTRequest, + envVars: Environment["variables"] +) => { + // If Authorization header is also being user-defined, that takes priority + if (req.headers.find((h) => h.key.toLowerCase() === "authorization")) + return [] + + if (!req.auth.authActive) return [] + + const headers: HoppRESTHeader[] = [] + + // TODO: Support a better b64 implementation than btoa ? + if (req.auth.authType === "basic") { + const username = parseTemplateString(req.auth.username, envVars) + const password = parseTemplateString(req.auth.password, envVars) + + headers.push({ + active: true, + key: "Authorization", + value: `Basic ${btoa(`${username}:${password}`)}`, + }) + } else if ( + req.auth.authType === "bearer" || + req.auth.authType === "oauth-2" + ) { + headers.push({ + active: true, + key: "Authorization", + value: `Bearer ${parseTemplateString(req.auth.token, envVars)}`, + }) + } else if (req.auth.authType === "api-key") { + const { key, value, addTo } = req.auth + + if (addTo === "Headers") { + headers.push({ + active: true, + key: parseTemplateString(key, envVars), + value: parseTemplateString(value, envVars), + }) + } + } + + return headers +} + +/** + * Get headers that can be generated by body config of the request + * @param req Request to check + * @returns The list of headers + */ +export const getComputedBodyHeaders = ( + req: HoppRESTRequest +): HoppRESTHeader[] => { + // If a content-type is already defined, that will override this + if ( + req.headers.find( + (req) => req.active && req.key.toLowerCase() === "content-type" + ) + ) + return [] + + // Body should have a non-null content-type + if (req.body.contentType === null) return [] + + return [ + { + active: true, + key: "content-type", + value: req.body.contentType, + }, + ] +} + +export type ComputedHeader = { + source: "auth" | "body" + header: HoppRESTHeader +} + +/** + * Returns a list of headers that will be added during execution of the request + * For e.g, Authorization headers maybe added if an Auth Mode is defined on REST + * @param req The request to check + * @param envVars The environment variables active + * @returns The headers that are generated along with the source of that header + */ +export const getComputedHeaders = ( + req: HoppRESTRequest, + envVars: Environment["variables"] +): ComputedHeader[] => [ + ...getComputedAuthHeaders(req, envVars).map((header) => ({ + source: "auth" as const, + header, + })), + ...getComputedBodyHeaders(req).map((header) => ({ + source: "body" as const, + header, + })), +] + +export type ComputedParam = { + source: "auth" + param: HoppRESTParam +} + +/** + * Returns a list of params that will be added during execution of the request + * For e.g, Authorization params (like API-key) maybe added if an Auth Mode is defined on REST + * @param req The request to check + * @param envVars The environment variables active + * @returns The params that are generated along with the source of that header + */ +export const getComputedParams = ( + req: HoppRESTRequest, + envVars: Environment["variables"] +): ComputedParam[] => { + // When this gets complex, its best to split this function off (like with getComputedHeaders) + // API-key auth can be added to query params + if (!req.auth.authActive) return [] + if (req.auth.authType !== "api-key") return [] + if (req.auth.addTo !== "Query params") return [] + + return [ + { + source: "auth", + param: { + active: true, + key: parseTemplateString(req.auth.key, envVars), + value: parseTemplateString(req.auth.value, envVars), + }, + }, + ] +} + // Resolves environment variables in the body export const resolvesEnvsInBody = ( body: HoppRESTReqBody, @@ -135,83 +277,29 @@ export function getEffectiveRESTRequest( ): EffectiveHoppRESTRequest { const envVariables = [...environment.variables, ...getGlobalVariables()] - const effectiveFinalHeaders = request.headers - .filter( - (x) => - x.key !== "" && // Remove empty keys - x.active // Only active - ) - .map((x) => ({ - // Parse out environment template strings + const effectiveFinalHeaders = pipe( + getComputedHeaders(request, envVariables).map((h) => h.header), + A.concat(request.headers), + A.filter((x) => x.active && x.key !== ""), + A.map((x) => ({ active: true, key: parseTemplateString(x.key, envVariables), value: parseTemplateString(x.value, envVariables), })) + ) - const effectiveFinalParams = request.params - .filter( - (x) => - x.key !== "" && // Remove empty keys - x.active // Only active - ) - .map((x) => ({ + const effectiveFinalParams = pipe( + getComputedParams(request, envVariables).map((p) => p.param), + A.concat(request.params), + A.filter((x) => x.active && x.key !== ""), + A.map((x) => ({ active: true, key: parseTemplateString(x.key, envVariables), value: parseTemplateString(x.value, envVariables), })) - - // Authentication - if (request.auth.authActive) { - // TODO: Support a better b64 implementation than btoa ? - if (request.auth.authType === "basic") { - const username = parseTemplateString(request.auth.username, envVariables) - const password = parseTemplateString(request.auth.password, envVariables) - - effectiveFinalHeaders.push({ - active: true, - key: "Authorization", - value: `Basic ${btoa(`${username}:${password}`)}`, - }) - } else if ( - request.auth.authType === "bearer" || - request.auth.authType === "oauth-2" - ) { - effectiveFinalHeaders.push({ - active: true, - key: "Authorization", - value: `Bearer ${parseTemplateString( - request.auth.token, - envVariables - )}`, - }) - } else if (request.auth.authType === "api-key") { - const { key, value, addTo } = request.auth - if (addTo === "Headers") { - effectiveFinalHeaders.push({ - active: true, - key: parseTemplateString(key, envVariables), - value: parseTemplateString(value, envVariables), - }) - } else if (addTo === "Query params") { - effectiveFinalParams.push({ - active: true, - key: parseTemplateString(key, envVariables), - value: parseTemplateString(value, envVariables), - }) - } - } - } + ) const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables) - const contentTypeInHeader = effectiveFinalHeaders.find( - (x) => x.key.toLowerCase() === "content-type" - ) - if (request.body.contentType && !contentTypeInHeader?.value) - effectiveFinalHeaders.push({ - active: true, - key: "content-type", - value: request.body.contentType, - }) return { ...request,