fix: ensure GraphQL connection sends authentication headers (#4746)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
d8875043cd
commit
903448186b
3 changed files with 210 additions and 113 deletions
|
|
@ -72,6 +72,7 @@ import { InterceptorService } from "~/services/interceptor.service"
|
|||
import { useService } from "dioc/vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
import { HoppGQLAuth, HoppGQLRequest } from "@hoppscotch/data"
|
||||
|
||||
const t = useI18n()
|
||||
const tabs = useService(GQLTabService)
|
||||
|
|
@ -98,7 +99,23 @@ const onConnectClick = () => {
|
|||
}
|
||||
|
||||
const gqlConnect = () => {
|
||||
connect(url.value, tabs.currentActiveTab.value?.document.request.headers)
|
||||
const inheritedHeaders =
|
||||
tabs.currentActiveTab.value.document.inheritedProperties?.headers.map(
|
||||
(header) => {
|
||||
if (header.inheritedHeader) {
|
||||
return header.inheritedHeader
|
||||
}
|
||||
return []
|
||||
}
|
||||
) as HoppGQLRequest["headers"]
|
||||
|
||||
connect({
|
||||
url: url.value,
|
||||
request: tabs.currentActiveTab.value.document.request,
|
||||
inheritedHeaders,
|
||||
inheritedAuth: tabs.currentActiveTab.value.document.inheritedProperties
|
||||
?.auth.inheritedAuth as HoppGQLAuth,
|
||||
})
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
|
|
|
|||
|
|
@ -57,26 +57,25 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { completePageProgress, startPageProgress } from "~/modules/loadingbar"
|
||||
import { HoppGQLAuth, HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { computedWithControl, useVModel } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import * as gql from "graphql"
|
||||
import { clone } from "lodash-es"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { platform } from "~/platform"
|
||||
import { computedWithControl, useVModel } from "@vueuse/core"
|
||||
import {
|
||||
connection,
|
||||
gqlMessageEvent,
|
||||
GQLResponseEvent,
|
||||
runGQLOperation,
|
||||
gqlMessageEvent,
|
||||
connection,
|
||||
} from "~/helpers/graphql/connection"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { editGraphqlRequest } from "~/newstore/collections"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
import { HoppRESTHeaders } from "@hoppscotch/data"
|
||||
import { completePageProgress, startPageProgress } from "~/modules/loadingbar"
|
||||
import { editGraphqlRequest } from "~/newstore/collections"
|
||||
import { platform } from "~/platform"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
const VALID_GQL_OPERATIONS = [
|
||||
"query",
|
||||
|
|
@ -139,13 +138,6 @@ const runQuery = async (
|
|||
const runURL = clone(url.value)
|
||||
const runQuery = clone(request.value.query)
|
||||
const runVariables = clone(request.value.variables)
|
||||
const runAuth =
|
||||
request.value.auth.authType === "inherit" && request.value.auth.authActive
|
||||
? clone(
|
||||
tabs.currentActiveTab.value.document.inheritedProperties?.auth
|
||||
.inheritedAuth
|
||||
)
|
||||
: clone(request.value.auth)
|
||||
|
||||
const inheritedHeaders =
|
||||
tabs.currentActiveTab.value.document.inheritedProperties?.headers.map(
|
||||
|
|
@ -155,26 +147,17 @@ const runQuery = async (
|
|||
}
|
||||
return []
|
||||
}
|
||||
)
|
||||
|
||||
let runHeaders: HoppGQLRequest["headers"] = []
|
||||
|
||||
if (inheritedHeaders) {
|
||||
runHeaders = [
|
||||
...inheritedHeaders,
|
||||
...clone(request.value.headers),
|
||||
] as HoppRESTHeaders
|
||||
} else {
|
||||
runHeaders = clone(request.value.headers)
|
||||
}
|
||||
) as HoppGQLRequest["headers"]
|
||||
|
||||
await runGQLOperation({
|
||||
name: request.value.name,
|
||||
url: runURL,
|
||||
headers: runHeaders,
|
||||
request: request.value,
|
||||
inheritedHeaders: inheritedHeaders,
|
||||
inheritedAuth: tabs.currentActiveTab.value.document.inheritedProperties
|
||||
?.auth.inheritedAuth as HoppGQLAuth,
|
||||
query: runQuery,
|
||||
variables: runVariables,
|
||||
auth: runAuth ?? { authType: "none", authActive: false },
|
||||
operationName: definition?.name?.value,
|
||||
operationType: definition?.operation ?? "query",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { GQLHeader, HoppGQLAuth, makeGQLRequest } from "@hoppscotch/data"
|
||||
import {
|
||||
HoppGQLAuth,
|
||||
HoppGQLRequest,
|
||||
HoppRESTHeaders,
|
||||
makeGQLRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { OperationType } from "@urql/core"
|
||||
import { AwsV4Signer } from "aws4fetch"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
|
@ -12,6 +17,7 @@ import {
|
|||
getIntrospectionQuery,
|
||||
printSchema,
|
||||
} from "graphql"
|
||||
import { clone } from "lodash-es"
|
||||
import { Component, computed, reactive, ref } from "vue"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { getService } from "~/modules/dioc"
|
||||
|
|
@ -24,13 +30,21 @@ import { GQLTabService } from "~/services/tab/graphql"
|
|||
|
||||
const GQL_SCHEMA_POLL_INTERVAL = 7000
|
||||
|
||||
type ConnectionRequestOptions = {
|
||||
url: string
|
||||
request: HoppGQLRequest
|
||||
inheritedHeaders: HoppGQLRequest["headers"]
|
||||
inheritedAuth: HoppGQLAuth
|
||||
}
|
||||
|
||||
type RunQueryOptions = {
|
||||
name?: string
|
||||
url: string
|
||||
headers: GQLHeader[]
|
||||
request: HoppGQLRequest
|
||||
inheritedHeaders: HoppGQLRequest["headers"]
|
||||
inheritedAuth: HoppGQLAuth
|
||||
query: string
|
||||
variables: string
|
||||
auth: HoppGQLAuth
|
||||
operationName: string | undefined
|
||||
operationType: OperationType
|
||||
}
|
||||
|
|
@ -162,8 +176,7 @@ export const graphqlTypes = computed(() => {
|
|||
let timeoutSubscription: any
|
||||
|
||||
export const connect = async (
|
||||
url: string,
|
||||
headers: GQLHeader[],
|
||||
options: ConnectionRequestOptions,
|
||||
isRunGQLOperation = false
|
||||
) => {
|
||||
if (connection.state === "CONNECTED") {
|
||||
|
|
@ -179,7 +192,7 @@ export const connect = async (
|
|||
|
||||
const poll = async () => {
|
||||
try {
|
||||
await getSchema(url, headers)
|
||||
await getSchema(options)
|
||||
// polling for schema
|
||||
if (connection.state !== "CONNECTED") connection.state = "CONNECTED"
|
||||
timeoutSubscription = setTimeout(() => {
|
||||
|
|
@ -217,20 +230,50 @@ export const reset = () => {
|
|||
connection.schema = null
|
||||
}
|
||||
|
||||
const getSchema = async (url: string, headers: GQLHeader[]) => {
|
||||
const getSchema = async (options: ConnectionRequestOptions) => {
|
||||
try {
|
||||
const introspectionQuery = JSON.stringify({
|
||||
query: getIntrospectionQuery(),
|
||||
})
|
||||
|
||||
const { url, request, inheritedHeaders, inheritedAuth } = options
|
||||
|
||||
const headers = request?.headers || []
|
||||
|
||||
const auth =
|
||||
request?.auth.authType === "inherit" && request.auth.authActive
|
||||
? clone(inheritedAuth)
|
||||
: clone(request.auth)
|
||||
|
||||
let runHeaders: HoppGQLRequest["headers"] = []
|
||||
|
||||
if (inheritedHeaders) {
|
||||
runHeaders = [
|
||||
...inheritedHeaders,
|
||||
...clone(request.headers),
|
||||
] as HoppRESTHeaders
|
||||
} else {
|
||||
runHeaders = clone(request.headers)
|
||||
}
|
||||
|
||||
const finalHeaders: Record<string, string> = {}
|
||||
|
||||
const { authHeaders } = await generateAuthHeader(url, auth)
|
||||
|
||||
runHeaders.forEach((header) => {
|
||||
if (header.active && header.key !== "") {
|
||||
finalHeaders[header.key] = header.value
|
||||
}
|
||||
})
|
||||
Object.assign(finalHeaders, authHeaders)
|
||||
|
||||
headers
|
||||
.filter((x) => x.active && x.key !== "")
|
||||
.forEach((x) => (finalHeaders[x.key] = x.value))
|
||||
.filter((item) => item.active && item.key !== "")
|
||||
.forEach(({ key, value }) => (finalHeaders[key] = value))
|
||||
|
||||
const reqOptions = {
|
||||
method: "POST",
|
||||
url,
|
||||
url: options.url,
|
||||
headers: {
|
||||
...finalHeaders,
|
||||
"content-type": "application/json",
|
||||
|
|
@ -293,19 +336,135 @@ const getSchema = async (url: string, headers: GQLHeader[]) => {
|
|||
|
||||
export const runGQLOperation = async (options: RunQueryOptions) => {
|
||||
if (connection.state !== "CONNECTED") {
|
||||
await connect(options.url, options.headers, true)
|
||||
await connect(
|
||||
{
|
||||
url: options.url,
|
||||
request: options.request,
|
||||
inheritedHeaders: options.inheritedHeaders,
|
||||
inheritedAuth: options.inheritedAuth,
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
const { url, headers, query, variables, auth, operationName, operationType } =
|
||||
options
|
||||
const {
|
||||
url,
|
||||
request,
|
||||
query,
|
||||
variables,
|
||||
operationName,
|
||||
inheritedHeaders,
|
||||
inheritedAuth,
|
||||
operationType,
|
||||
} = options
|
||||
|
||||
const headers = request?.headers || []
|
||||
|
||||
const auth =
|
||||
request?.auth.authType === "inherit" && request.auth.authActive
|
||||
? clone(inheritedAuth)
|
||||
: clone(request.auth)
|
||||
|
||||
let runHeaders: HoppGQLRequest["headers"] = []
|
||||
|
||||
if (inheritedHeaders) {
|
||||
runHeaders = [
|
||||
...inheritedHeaders,
|
||||
...clone(request.headers),
|
||||
] as HoppRESTHeaders
|
||||
} else {
|
||||
runHeaders = clone(request.headers)
|
||||
}
|
||||
|
||||
const finalHeaders: Record<string, string> = {}
|
||||
|
||||
const { authHeaders, authParams } = await generateAuthHeader(url, auth)
|
||||
|
||||
runHeaders.forEach((header) => {
|
||||
if (header.active && header.key !== "") {
|
||||
finalHeaders[header.key] = header.value
|
||||
}
|
||||
})
|
||||
Object.assign(finalHeaders, authHeaders)
|
||||
|
||||
const parsedVariables = JSON.parse(variables || "{}")
|
||||
|
||||
const params: Record<string, string> = {}
|
||||
|
||||
if (auth.authActive) {
|
||||
headers
|
||||
.filter((item) => item.active && item.key !== "")
|
||||
.forEach(({ key, value }) => (finalHeaders[key] = value))
|
||||
|
||||
const reqOptions = {
|
||||
method: "POST",
|
||||
url,
|
||||
headers: {
|
||||
...finalHeaders,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
data: JSON.stringify({
|
||||
query,
|
||||
variables: parsedVariables,
|
||||
operationName,
|
||||
}),
|
||||
params: {
|
||||
...params,
|
||||
...authParams,
|
||||
},
|
||||
}
|
||||
|
||||
if (operationType === "subscription") {
|
||||
return runSubscription(options, finalHeaders)
|
||||
}
|
||||
|
||||
const interceptorService = getService(InterceptorService)
|
||||
const result = await interceptorService.runRequest(reqOptions).response
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
if (
|
||||
result.left !== "cancellation" &&
|
||||
result.left.error === "NO_PW_EXT_HOOK" &&
|
||||
result.left.humanMessage
|
||||
) {
|
||||
connection.error = {
|
||||
type: result.left.error,
|
||||
message: (t: ReturnType<typeof getI18n>) =>
|
||||
result.left.humanMessage.description(t),
|
||||
component: result.left.component,
|
||||
}
|
||||
}
|
||||
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 as any)
|
||||
.replace(/\0+$/, "")
|
||||
|
||||
gqlMessageEvent.value = {
|
||||
type: "response",
|
||||
time: Date.now(),
|
||||
operationName: operationName ?? "query",
|
||||
data: responseText,
|
||||
rawQuery: options,
|
||||
operationType,
|
||||
}
|
||||
|
||||
addQueryToHistory(options, responseText)
|
||||
|
||||
return responseText
|
||||
}
|
||||
|
||||
const generateAuthHeader = async (
|
||||
url: string,
|
||||
auth: HoppGQLAuth | undefined
|
||||
) => {
|
||||
const finalHeaders: Record<string, string> = {}
|
||||
const params: Record<string, string> = {}
|
||||
|
||||
if (auth?.authActive) {
|
||||
if (auth.authType === "basic") {
|
||||
const username = auth.username
|
||||
const password = auth.password
|
||||
|
|
@ -359,69 +518,7 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
|
|||
}
|
||||
}
|
||||
|
||||
headers
|
||||
.filter((item) => item.active && item.key !== "")
|
||||
.forEach(({ key, value }) => (finalHeaders[key] = value))
|
||||
|
||||
const reqOptions = {
|
||||
method: "POST",
|
||||
url,
|
||||
headers: {
|
||||
...finalHeaders,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
data: JSON.stringify({
|
||||
query,
|
||||
variables: parsedVariables,
|
||||
operationName,
|
||||
}),
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
}
|
||||
|
||||
if (operationType === "subscription") {
|
||||
return runSubscription(options, finalHeaders)
|
||||
}
|
||||
|
||||
const interceptorService = getService(InterceptorService)
|
||||
const result = await interceptorService.runRequest(reqOptions).response
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
if (
|
||||
result.left !== "cancellation" &&
|
||||
result.left.error === "NO_PW_EXT_HOOK" &&
|
||||
result.left.humanMessage
|
||||
) {
|
||||
connection.error = {
|
||||
type: result.left.error,
|
||||
message: (t: ReturnType<typeof getI18n>) =>
|
||||
result.left.humanMessage.description(t),
|
||||
component: result.left.component,
|
||||
}
|
||||
}
|
||||
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 as any)
|
||||
.replace(/\0+$/, "")
|
||||
|
||||
gqlMessageEvent.value = {
|
||||
type: "response",
|
||||
time: Date.now(),
|
||||
operationName: operationName ?? "query",
|
||||
data: responseText,
|
||||
rawQuery: options,
|
||||
operationType,
|
||||
}
|
||||
|
||||
addQueryToHistory(options, responseText)
|
||||
|
||||
return responseText
|
||||
return { authHeaders: finalHeaders, authParams: params }
|
||||
}
|
||||
|
||||
export const runSubscription = (
|
||||
|
|
@ -502,16 +599,16 @@ export const socketDisconnect = () => {
|
|||
}
|
||||
|
||||
const addQueryToHistory = (options: RunQueryOptions, response: string) => {
|
||||
const { name, url, headers, query, variables, auth } = options
|
||||
const { name, url, request, query, variables } = options
|
||||
addGraphqlHistoryEntry(
|
||||
makeGQLHistoryEntry({
|
||||
request: makeGQLRequest({
|
||||
name: name ?? "Untitled Request",
|
||||
url,
|
||||
query,
|
||||
headers,
|
||||
headers: request.headers,
|
||||
variables,
|
||||
auth,
|
||||
auth: request.auth as HoppGQLAuth,
|
||||
}),
|
||||
response,
|
||||
star: false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue