refactor: isolate computed header calculation on effective requests (#2313)

Co-authored-by: liyasthomas <liyascthomas@gmail.com>
This commit is contained in:
Andrew Bastin
2022-05-11 14:53:06 +05:30
committed by GitHub
parent 450af983e2
commit d04520698d
4 changed files with 263 additions and 70 deletions

View File

@@ -137,6 +137,47 @@
/>
</span>
</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>
<div
v-if="workingHeaders.length === 0"
@@ -162,7 +203,7 @@
</template>
<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 {
HoppRESTHeader,
@@ -177,13 +218,29 @@ import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import cloneDeep from "lodash/cloneDeep"
import draggable from "vuedraggable"
import { RequestOptionTabs } from "./RequestOptions.vue"
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 { useI18n, useStream, useToast } from "~/helpers/utils/composables"
import {
useI18n,
useReadonlyStream,
useStream,
useToast,
} from "~/helpers/utils/composables"
import linter from "~/helpers/editor/linting/rawKeyValue"
import { throwError } from "~/helpers/functional/error"
import { objRemoveKey } from "~/helpers/functional/object"
import {
ComputedHeader,
getComputedHeaders,
} from "~/helpers/utils/EffectiveURL"
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
const t = useI18n()
const toast = useToast()
@@ -196,6 +253,10 @@ const bulkEditor = ref<any | null>(null)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const emit = defineEmits<{
(e: "change-tab", value: RequestOptionTabs): void
}>()
useCodemirror(bulkEditor, bulkHeaders, {
extendedEditorConfig: {
mode: "text/x-yaml",
@@ -379,4 +440,28 @@ const clearContent = () => {
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>

View File

@@ -18,7 +18,7 @@
:label="`${$t('tab.headers')}`"
:info="`${newActiveHeadersCount$}`"
>
<HttpHeaders />
<HttpHeaders @change-tab="changeTab" />
</SmartTab>
<SmartTab :id="'authorization'" :label="`${$t('tab.authorization')}`">
<HttpAuthorization />

View File

@@ -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)

View File

@@ -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,