refactor: isolate computed header calculation on effective requests (#2313)
Co-authored-by: liyasthomas <liyascthomas@gmail.com>
This commit is contained in:
@@ -137,6 +137,47 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(header, index) in computedHeaders"
|
||||||
|
:key="`header-${index}`"
|
||||||
|
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<ButtonSecondary
|
||||||
|
svg="lock"
|
||||||
|
class="opacity-25 cursor-auto text-secondaryLight bg-divider"
|
||||||
|
tabindex="-1"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="header.header.key"
|
||||||
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<SmartEnvInput
|
||||||
|
:value="mask(header)"
|
||||||
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-if="header.source === 'auth'"
|
||||||
|
:svg="masking ? 'eye' : 'eye-off'"
|
||||||
|
@click.native="toggleMask()"
|
||||||
|
/>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-else
|
||||||
|
svg="arrow-up-right"
|
||||||
|
class="cursor-auto text-primary hover:text-primary"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<ButtonSecondary
|
||||||
|
svg="arrow-up-right"
|
||||||
|
@click.native="changeTab(header.source)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</draggable>
|
</draggable>
|
||||||
<div
|
<div
|
||||||
v-if="workingHeaders.length === 0"
|
v-if="workingHeaders.length === 0"
|
||||||
@@ -162,7 +203,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Ref, ref, watch } from "@nuxtjs/composition-api"
|
import { computed, Ref, ref, watch } from "@nuxtjs/composition-api"
|
||||||
import isEqual from "lodash/isEqual"
|
import isEqual from "lodash/isEqual"
|
||||||
import {
|
import {
|
||||||
HoppRESTHeader,
|
HoppRESTHeader,
|
||||||
@@ -177,13 +218,29 @@ import * as O from "fp-ts/Option"
|
|||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import cloneDeep from "lodash/cloneDeep"
|
import cloneDeep from "lodash/cloneDeep"
|
||||||
import draggable from "vuedraggable"
|
import draggable from "vuedraggable"
|
||||||
|
import { RequestOptionTabs } from "./RequestOptions.vue"
|
||||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { restHeaders$, setRESTHeaders } from "~/newstore/RESTSession"
|
import {
|
||||||
|
getRESTRequest,
|
||||||
|
restHeaders$,
|
||||||
|
restRequest$,
|
||||||
|
setRESTHeaders,
|
||||||
|
} from "~/newstore/RESTSession"
|
||||||
import { commonHeaders } from "~/helpers/headers"
|
import { commonHeaders } from "~/helpers/headers"
|
||||||
import { useI18n, useStream, useToast } from "~/helpers/utils/composables"
|
import {
|
||||||
|
useI18n,
|
||||||
|
useReadonlyStream,
|
||||||
|
useStream,
|
||||||
|
useToast,
|
||||||
|
} from "~/helpers/utils/composables"
|
||||||
import linter from "~/helpers/editor/linting/rawKeyValue"
|
import linter from "~/helpers/editor/linting/rawKeyValue"
|
||||||
import { throwError } from "~/helpers/functional/error"
|
import { throwError } from "~/helpers/functional/error"
|
||||||
import { objRemoveKey } from "~/helpers/functional/object"
|
import { objRemoveKey } from "~/helpers/functional/object"
|
||||||
|
import {
|
||||||
|
ComputedHeader,
|
||||||
|
getComputedHeaders,
|
||||||
|
} from "~/helpers/utils/EffectiveURL"
|
||||||
|
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -196,6 +253,10 @@ const bulkEditor = ref<any | null>(null)
|
|||||||
|
|
||||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "change-tab", value: RequestOptionTabs): void
|
||||||
|
}>()
|
||||||
|
|
||||||
useCodemirror(bulkEditor, bulkHeaders, {
|
useCodemirror(bulkEditor, bulkHeaders, {
|
||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "text/x-yaml",
|
mode: "text/x-yaml",
|
||||||
@@ -379,4 +440,28 @@ const clearContent = () => {
|
|||||||
|
|
||||||
bulkHeaders.value = ""
|
bulkHeaders.value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const restRequest = useReadonlyStream(restRequest$, getRESTRequest())
|
||||||
|
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
|
||||||
|
|
||||||
|
const computedHeaders = computed(() =>
|
||||||
|
getComputedHeaders(restRequest.value, aggregateEnvs.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const masking = ref(true)
|
||||||
|
|
||||||
|
const toggleMask = () => {
|
||||||
|
masking.value = !masking.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const mask = (header: ComputedHeader) => {
|
||||||
|
if (header.source === "auth" && masking.value)
|
||||||
|
return header.header.value.replace(/\S/gi, "*")
|
||||||
|
return header.header.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeTab = (tab: ComputedHeader["source"]) => {
|
||||||
|
if (tab === "auth") emit("change-tab", "authorization")
|
||||||
|
else emit("change-tab", "bodyParams")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
:label="`${$t('tab.headers')}`"
|
:label="`${$t('tab.headers')}`"
|
||||||
:info="`${newActiveHeadersCount$}`"
|
:info="`${newActiveHeadersCount$}`"
|
||||||
>
|
>
|
||||||
<HttpHeaders />
|
<HttpHeaders @change-tab="changeTab" />
|
||||||
</SmartTab>
|
</SmartTab>
|
||||||
<SmartTab :id="'authorization'" :label="`${$t('tab.authorization')}`">
|
<SmartTab :id="'authorization'" :label="`${$t('tab.authorization')}`">
|
||||||
<HttpAuthorization />
|
<HttpAuthorization />
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const props = withDefaults(
|
|||||||
styles: string
|
styles: string
|
||||||
envs: { key: string; value: string; source: string }[] | null
|
envs: { key: string; value: string; source: string }[] | null
|
||||||
focus: boolean
|
focus: boolean
|
||||||
|
readonly: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
value: "",
|
value: "",
|
||||||
@@ -54,6 +55,7 @@ const props = withDefaults(
|
|||||||
styles: "",
|
styles: "",
|
||||||
envs: null,
|
envs: null,
|
||||||
focus: false,
|
focus: false,
|
||||||
|
readonly: false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -123,7 +125,23 @@ const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
|||||||
const initView = (el: any) => {
|
const initView = (el: any) => {
|
||||||
const extensions: Extension = [
|
const extensions: Extension = [
|
||||||
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
|
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,
|
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({
|
tooltips({
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
}),
|
}),
|
||||||
@@ -141,6 +159,8 @@ const initView = (el: any) => {
|
|||||||
ViewPlugin.fromClass(
|
ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
update(update: ViewUpdate) {
|
update(update: ViewUpdate) {
|
||||||
|
if (props.readonly) return
|
||||||
|
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
const prevValue = clone(cachedValue.value)
|
const prevValue = clone(cachedValue.value)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
parseBodyEnvVariables,
|
parseBodyEnvVariables,
|
||||||
parseRawKeyValueEntries,
|
parseRawKeyValueEntries,
|
||||||
Environment,
|
Environment,
|
||||||
|
HoppRESTHeader,
|
||||||
|
HoppRESTParam,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import { arrayFlatMap, arraySort } from "../functional/array"
|
import { arrayFlatMap, arraySort } from "../functional/array"
|
||||||
import { toFormData } from "../functional/formData"
|
import { toFormData } from "../functional/formData"
|
||||||
@@ -29,6 +31,146 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
|||||||
effectiveFinalBody: FormData | string | null
|
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
|
// Resolves environment variables in the body
|
||||||
export const resolvesEnvsInBody = (
|
export const resolvesEnvsInBody = (
|
||||||
body: HoppRESTReqBody,
|
body: HoppRESTReqBody,
|
||||||
@@ -135,83 +277,29 @@ export function getEffectiveRESTRequest(
|
|||||||
): EffectiveHoppRESTRequest {
|
): EffectiveHoppRESTRequest {
|
||||||
const envVariables = [...environment.variables, ...getGlobalVariables()]
|
const envVariables = [...environment.variables, ...getGlobalVariables()]
|
||||||
|
|
||||||
const effectiveFinalHeaders = request.headers
|
const effectiveFinalHeaders = pipe(
|
||||||
.filter(
|
getComputedHeaders(request, envVariables).map((h) => h.header),
|
||||||
(x) =>
|
A.concat(request.headers),
|
||||||
x.key !== "" && // Remove empty keys
|
A.filter((x) => x.active && x.key !== ""),
|
||||||
x.active // Only active
|
A.map((x) => ({
|
||||||
)
|
|
||||||
.map((x) => ({
|
|
||||||
// Parse out environment template strings
|
|
||||||
active: true,
|
active: true,
|
||||||
key: parseTemplateString(x.key, envVariables),
|
key: parseTemplateString(x.key, envVariables),
|
||||||
value: parseTemplateString(x.value, envVariables),
|
value: parseTemplateString(x.value, envVariables),
|
||||||
}))
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const effectiveFinalParams = request.params
|
const effectiveFinalParams = pipe(
|
||||||
.filter(
|
getComputedParams(request, envVariables).map((p) => p.param),
|
||||||
(x) =>
|
A.concat(request.params),
|
||||||
x.key !== "" && // Remove empty keys
|
A.filter((x) => x.active && x.key !== ""),
|
||||||
x.active // Only active
|
A.map((x) => ({
|
||||||
)
|
|
||||||
.map((x) => ({
|
|
||||||
active: true,
|
active: true,
|
||||||
key: parseTemplateString(x.key, envVariables),
|
key: parseTemplateString(x.key, envVariables),
|
||||||
value: parseTemplateString(x.value, 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 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 {
|
return {
|
||||||
...request,
|
...request,
|
||||||
|
|||||||
Reference in New Issue
Block a user