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 { useService } from "dioc/vue"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { GQLTabService } from "~/services/tab/graphql"
|
import { GQLTabService } from "~/services/tab/graphql"
|
||||||
|
import { HoppGQLAuth, HoppGQLRequest } from "@hoppscotch/data"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const tabs = useService(GQLTabService)
|
const tabs = useService(GQLTabService)
|
||||||
|
|
@ -98,7 +99,23 @@ const onConnectClick = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const gqlConnect = () => {
|
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({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_REQUEST_RUN",
|
type: "HOPP_REQUEST_RUN",
|
||||||
|
|
|
||||||
|
|
@ -57,26 +57,25 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
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 * as gql from "graphql"
|
||||||
import { clone } from "lodash-es"
|
import { clone } from "lodash-es"
|
||||||
import { computed, ref, watch } from "vue"
|
import { computed, ref, watch } from "vue"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
|
||||||
import { platform } from "~/platform"
|
|
||||||
import { computedWithControl, useVModel } from "@vueuse/core"
|
|
||||||
import {
|
import {
|
||||||
|
connection,
|
||||||
|
gqlMessageEvent,
|
||||||
GQLResponseEvent,
|
GQLResponseEvent,
|
||||||
runGQLOperation,
|
runGQLOperation,
|
||||||
gqlMessageEvent,
|
|
||||||
connection,
|
|
||||||
} from "~/helpers/graphql/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 { 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 = [
|
const VALID_GQL_OPERATIONS = [
|
||||||
"query",
|
"query",
|
||||||
|
|
@ -139,13 +138,6 @@ const runQuery = async (
|
||||||
const runURL = clone(url.value)
|
const runURL = clone(url.value)
|
||||||
const runQuery = clone(request.value.query)
|
const runQuery = clone(request.value.query)
|
||||||
const runVariables = clone(request.value.variables)
|
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 =
|
const inheritedHeaders =
|
||||||
tabs.currentActiveTab.value.document.inheritedProperties?.headers.map(
|
tabs.currentActiveTab.value.document.inheritedProperties?.headers.map(
|
||||||
|
|
@ -155,26 +147,17 @@ const runQuery = async (
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
)
|
) as HoppGQLRequest["headers"]
|
||||||
|
|
||||||
let runHeaders: HoppGQLRequest["headers"] = []
|
|
||||||
|
|
||||||
if (inheritedHeaders) {
|
|
||||||
runHeaders = [
|
|
||||||
...inheritedHeaders,
|
|
||||||
...clone(request.value.headers),
|
|
||||||
] as HoppRESTHeaders
|
|
||||||
} else {
|
|
||||||
runHeaders = clone(request.value.headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
await runGQLOperation({
|
await runGQLOperation({
|
||||||
name: request.value.name,
|
name: request.value.name,
|
||||||
url: runURL,
|
url: runURL,
|
||||||
headers: runHeaders,
|
request: request.value,
|
||||||
|
inheritedHeaders: inheritedHeaders,
|
||||||
|
inheritedAuth: tabs.currentActiveTab.value.document.inheritedProperties
|
||||||
|
?.auth.inheritedAuth as HoppGQLAuth,
|
||||||
query: runQuery,
|
query: runQuery,
|
||||||
variables: runVariables,
|
variables: runVariables,
|
||||||
auth: runAuth ?? { authType: "none", authActive: false },
|
|
||||||
operationName: definition?.name?.value,
|
operationName: definition?.name?.value,
|
||||||
operationType: definition?.operation ?? "query",
|
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 { OperationType } from "@urql/core"
|
||||||
import { AwsV4Signer } from "aws4fetch"
|
import { AwsV4Signer } from "aws4fetch"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
|
|
@ -12,6 +17,7 @@ import {
|
||||||
getIntrospectionQuery,
|
getIntrospectionQuery,
|
||||||
printSchema,
|
printSchema,
|
||||||
} from "graphql"
|
} from "graphql"
|
||||||
|
import { clone } from "lodash-es"
|
||||||
import { Component, computed, reactive, ref } from "vue"
|
import { Component, computed, reactive, ref } from "vue"
|
||||||
import { useToast } from "~/composables/toast"
|
import { useToast } from "~/composables/toast"
|
||||||
import { getService } from "~/modules/dioc"
|
import { getService } from "~/modules/dioc"
|
||||||
|
|
@ -24,13 +30,21 @@ import { GQLTabService } from "~/services/tab/graphql"
|
||||||
|
|
||||||
const GQL_SCHEMA_POLL_INTERVAL = 7000
|
const GQL_SCHEMA_POLL_INTERVAL = 7000
|
||||||
|
|
||||||
|
type ConnectionRequestOptions = {
|
||||||
|
url: string
|
||||||
|
request: HoppGQLRequest
|
||||||
|
inheritedHeaders: HoppGQLRequest["headers"]
|
||||||
|
inheritedAuth: HoppGQLAuth
|
||||||
|
}
|
||||||
|
|
||||||
type RunQueryOptions = {
|
type RunQueryOptions = {
|
||||||
name?: string
|
name?: string
|
||||||
url: string
|
url: string
|
||||||
headers: GQLHeader[]
|
request: HoppGQLRequest
|
||||||
|
inheritedHeaders: HoppGQLRequest["headers"]
|
||||||
|
inheritedAuth: HoppGQLAuth
|
||||||
query: string
|
query: string
|
||||||
variables: string
|
variables: string
|
||||||
auth: HoppGQLAuth
|
|
||||||
operationName: string | undefined
|
operationName: string | undefined
|
||||||
operationType: OperationType
|
operationType: OperationType
|
||||||
}
|
}
|
||||||
|
|
@ -162,8 +176,7 @@ export const graphqlTypes = computed(() => {
|
||||||
let timeoutSubscription: any
|
let timeoutSubscription: any
|
||||||
|
|
||||||
export const connect = async (
|
export const connect = async (
|
||||||
url: string,
|
options: ConnectionRequestOptions,
|
||||||
headers: GQLHeader[],
|
|
||||||
isRunGQLOperation = false
|
isRunGQLOperation = false
|
||||||
) => {
|
) => {
|
||||||
if (connection.state === "CONNECTED") {
|
if (connection.state === "CONNECTED") {
|
||||||
|
|
@ -179,7 +192,7 @@ export const connect = async (
|
||||||
|
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
try {
|
try {
|
||||||
await getSchema(url, headers)
|
await getSchema(options)
|
||||||
// polling for schema
|
// polling for schema
|
||||||
if (connection.state !== "CONNECTED") connection.state = "CONNECTED"
|
if (connection.state !== "CONNECTED") connection.state = "CONNECTED"
|
||||||
timeoutSubscription = setTimeout(() => {
|
timeoutSubscription = setTimeout(() => {
|
||||||
|
|
@ -217,20 +230,50 @@ export const reset = () => {
|
||||||
connection.schema = null
|
connection.schema = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSchema = async (url: string, headers: GQLHeader[]) => {
|
const getSchema = async (options: ConnectionRequestOptions) => {
|
||||||
try {
|
try {
|
||||||
const introspectionQuery = JSON.stringify({
|
const introspectionQuery = JSON.stringify({
|
||||||
query: getIntrospectionQuery(),
|
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 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
|
headers
|
||||||
.filter((x) => x.active && x.key !== "")
|
.filter((item) => item.active && item.key !== "")
|
||||||
.forEach((x) => (finalHeaders[x.key] = x.value))
|
.forEach(({ key, value }) => (finalHeaders[key] = value))
|
||||||
|
|
||||||
const reqOptions = {
|
const reqOptions = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url,
|
url: options.url,
|
||||||
headers: {
|
headers: {
|
||||||
...finalHeaders,
|
...finalHeaders,
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
|
|
@ -293,19 +336,135 @@ const getSchema = async (url: string, headers: GQLHeader[]) => {
|
||||||
|
|
||||||
export const runGQLOperation = async (options: RunQueryOptions) => {
|
export const runGQLOperation = async (options: RunQueryOptions) => {
|
||||||
if (connection.state !== "CONNECTED") {
|
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 } =
|
const {
|
||||||
options
|
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 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 parsedVariables = JSON.parse(variables || "{}")
|
||||||
|
|
||||||
const params: Record<string, string> = {}
|
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") {
|
if (auth.authType === "basic") {
|
||||||
const username = auth.username
|
const username = auth.username
|
||||||
const password = auth.password
|
const password = auth.password
|
||||||
|
|
@ -359,69 +518,7 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
headers
|
return { authHeaders: finalHeaders, authParams: params }
|
||||||
.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const runSubscription = (
|
export const runSubscription = (
|
||||||
|
|
@ -502,16 +599,16 @@ export const socketDisconnect = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const addQueryToHistory = (options: RunQueryOptions, response: string) => {
|
const addQueryToHistory = (options: RunQueryOptions, response: string) => {
|
||||||
const { name, url, headers, query, variables, auth } = options
|
const { name, url, request, query, variables } = options
|
||||||
addGraphqlHistoryEntry(
|
addGraphqlHistoryEntry(
|
||||||
makeGQLHistoryEntry({
|
makeGQLHistoryEntry({
|
||||||
request: makeGQLRequest({
|
request: makeGQLRequest({
|
||||||
name: name ?? "Untitled Request",
|
name: name ?? "Untitled Request",
|
||||||
url,
|
url,
|
||||||
query,
|
query,
|
||||||
headers,
|
headers: request.headers,
|
||||||
variables,
|
variables,
|
||||||
auth,
|
auth: request.auth as HoppGQLAuth,
|
||||||
}),
|
}),
|
||||||
response,
|
response,
|
||||||
star: false,
|
star: false,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue