feat: oauth revamp + support for multiple grant types in oauth (#3885)

Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
Akash K
2024-03-20 00:18:03 +05:30
committed by GitHub
parent 457857a711
commit 6b58915caa
44 changed files with 2736 additions and 371 deletions

View File

@@ -8,7 +8,7 @@
>
<template #body>
<HoppSmartTabs
v-model="selectedOptionTab"
v-model="activeTab"
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10 !-py-4"
render-inactive-tabs
>
@@ -16,7 +16,6 @@
<HttpHeaders
v-model="editableCollection"
:is-collection-property="true"
@change-tab="changeOptionTab"
/>
<div
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
@@ -34,6 +33,7 @@
:is-collection-property="true"
:is-root-collection="editingProperties?.isRootCollection"
:inherited-properties="editingProperties?.inheritedProperties"
:source="source"
/>
<div
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
@@ -64,16 +64,20 @@
</template>
<script setup lang="ts">
import { watch, ref } from "vue"
import { useI18n } from "@composables/i18n"
import { HoppCollection, HoppRESTAuth, HoppRESTHeaders } from "@hoppscotch/data"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
import { clone } from "lodash-es"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { PersistenceService } from "~/services/persistence"
import { useService } from "dioc/vue"
import { ref, watch } from "vue"
import { useVModel } from "@vueuse/core"
const persistenceService = useService(PersistenceService)
const t = useI18n()
type EditingProperties = {
export type EditingProperties = {
collection: Partial<HoppCollection> | null
isRootCollection: boolean
path: string
@@ -85,6 +89,8 @@ const props = withDefaults(
show: boolean
loadingState: boolean
editingProperties: EditingProperties | null
source: "REST" | "GraphQL"
modelValue: string
}>(),
{
show: false,
@@ -99,6 +105,7 @@ const emit = defineEmits<{
newCollection: Omit<EditingProperties, "inheritedProperties">
): void
(e: "hide-modal"): void
(e: "update:modelValue"): void
}>()
const editableCollection = ref<{
@@ -112,11 +119,27 @@ const editableCollection = ref<{
},
})
const selectedOptionTab = ref("headers")
watch(
editableCollection,
(updatedEditableCollection) => {
if (props.show) {
persistenceService.setLocalConfig(
"unsaved_collection_properties",
JSON.stringify(<EditingProperties>{
collection: updatedEditableCollection,
isRootCollection: props.editingProperties?.isRootCollection,
path: props.editingProperties?.path,
inheritedProperties: props.editingProperties?.inheritedProperties,
})
)
}
},
{
deep: true,
}
)
const changeOptionTab = (tab: RESTOptionTabs) => {
selectedOptionTab.value = tab
}
const activeTab = useVModel(props, "modelValue", emit)
watch(
() => props.show,
@@ -136,6 +159,8 @@ watch(
authActive: false,
},
}
persistenceService.removeLocalConfig("unsaved_collection_properties")
}
}
)
@@ -152,9 +177,11 @@ const saveEditedCollection = () => {
isRootCollection: props.editingProperties.isRootCollection,
}
emit("set-collection-properties", collection as EditingProperties)
persistenceService.removeLocalConfig("unsaved_collection_properties")
}
const hideModal = () => {
persistenceService.removeLocalConfig("unsaved_collection_properties")
emit("hide-modal")
}
</script>

View File

@@ -146,8 +146,10 @@
@hide-modal="displayModalImportExport(false)"
/>
<CollectionsProperties
v-model="collectionPropertiesModalActiveTab"
:show="showModalEditProperties"
:editing-properties="editingProperties"
source="GraphQL"
@hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties"
/>
@@ -155,7 +157,7 @@
</template>
<script setup lang="ts">
import { nextTick, ref } from "vue"
import { nextTick, onMounted, ref } from "vue"
import { clone, cloneDeep } from "lodash-es"
import {
graphqlCollections$,
@@ -178,6 +180,7 @@ import { GQLTabService } from "~/services/tab/graphql"
import { computed } from "vue"
import {
HoppCollection,
HoppGQLAuth,
HoppGQLRequest,
makeGQLRequest,
} from "@hoppscotch/data"
@@ -186,6 +189,10 @@ import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { updateInheritedPropertiesForAffectedRequests } from "~/helpers/collection/collection"
import { useToast } from "~/composables/toast"
import { getRequestsByPath } from "~/helpers/collection/request"
import { PersistenceService } from "~/services/persistence"
import { PersistedOAuthConfig } from "~/services/oauth/oauth.service"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { EditingProperties } from "../Properties.vue"
const t = useI18n()
const toast = useToast()
@@ -232,6 +239,52 @@ const editingProperties = ref<{
const filterText = ref("")
const persistenceService = useService(PersistenceService)
const collectionPropertiesModalActiveTab = ref<GQLOptionTabs>("headers")
onMounted(() => {
const localOAuthTempConfig =
persistenceService.getLocalConfig("oauth_temp_config")
if (!localOAuthTempConfig) {
return
}
const { context, source, token }: PersistedOAuthConfig =
JSON.parse(localOAuthTempConfig)
if (source === "REST") {
return
}
if (context?.type === "collection-properties") {
// load the unsaved editing properties
const unsavedCollectionPropertiesString = persistenceService.getLocalConfig(
"unsaved_collection_properties"
)
if (unsavedCollectionPropertiesString) {
const unsavedCollectionProperties: EditingProperties<"GraphQL"> =
JSON.parse(unsavedCollectionPropertiesString)
const auth = unsavedCollectionProperties.collection?.auth
if (auth?.authType === "oauth-2") {
const grantTypeInfo = auth.grantTypeInfo
grantTypeInfo && (grantTypeInfo.token = token ?? "")
}
editingProperties.value = unsavedCollectionProperties
}
persistenceService.removeLocalConfig("oauth_temp_config")
collectionPropertiesModalActiveTab.value = "authorization"
showModalEditProperties.value = true
}
})
const filteredCollections = computed(() => {
const collectionsClone = clone(collections.value)

View File

@@ -161,8 +161,10 @@
@hide-modal="displayTeamModalAdd(false)"
/>
<CollectionsProperties
v-model="collectionPropertiesModalActiveTab"
:show="showModalEditProperties"
:editing-properties="editingProperties"
source="REST"
@hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties"
/>
@@ -170,7 +172,7 @@
</template>
<script setup lang="ts">
import { computed, nextTick, PropType, ref, watch } from "vue"
import { computed, nextTick, onMounted, PropType, ref, watch } from "vue"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { Picked } from "~/helpers/types/HoppPicked"
@@ -248,6 +250,10 @@ import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { TeamSearchService } from "~/helpers/teams/TeamsSearch.service"
import { PersistenceService } from "~/services/persistence"
import { PersistedOAuthConfig } from "~/services/oauth/oauth.service"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
import { EditingProperties } from "./Properties.vue"
const t = useI18n()
const toast = useToast()
@@ -299,12 +305,7 @@ const editingRequestName = ref("")
const editingRequestIndex = ref<number | null>(null)
const editingRequestID = ref<string | null>(null)
const editingProperties = ref<{
collection: Partial<HoppCollection> | null
isRootCollection: boolean
path: string
inheritedProperties?: HoppInheritedProperty
}>({
const editingProperties = ref<EditingProperties>({
collection: null,
isRootCollection: false,
path: "",
@@ -387,6 +388,55 @@ watch(
immediate: true,
}
)
const persistenceService = useService(PersistenceService)
const collectionPropertiesModalActiveTab = ref<RESTOptionTabs>("headers")
onMounted(() => {
const localOAuthTempConfig =
persistenceService.getLocalConfig("oauth_temp_config")
if (!localOAuthTempConfig) {
return
}
const { context, source, token }: PersistedOAuthConfig =
JSON.parse(localOAuthTempConfig)
if (source === "GraphQL") {
return
}
if (context?.type === "collection-properties") {
// load the unsaved editing properties
const unsavedCollectionPropertiesString = persistenceService.getLocalConfig(
"unsaved_collection_properties"
)
if (unsavedCollectionPropertiesString) {
const unsavedCollectionProperties: EditingProperties<"REST"> = JSON.parse(
unsavedCollectionPropertiesString
)
// casting because the type `EditingProperties["collection"]["auth"] and the usage in Properties.vue is different. there it's casted as an any.
// FUTURE-TODO: look into this
// @ts-expect-error because of the above reason
const auth = unsavedCollectionProperties.collection?.auth as HoppRESTAuth
if (auth?.authType === "oauth-2") {
const grantTypeInfo = auth.grantTypeInfo
grantTypeInfo && (grantTypeInfo.token = token ?? "")
}
editingProperties.value = unsavedCollectionProperties
}
persistenceService.removeLocalConfig("oauth_temp_config")
collectionPropertiesModalActiveTab.value = "authorization"
showModalEditProperties.value = true
}
})
watch(
() => myTeams.value,

View File

@@ -82,7 +82,7 @@
:active="authName === 'OAuth 2.0'"
@click="
() => {
auth.authType = 'oauth-2'
selectOAuth2AuthType()
hide()
}
"
@@ -189,12 +189,12 @@
<div v-if="auth.authType === 'oauth-2'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="auth.token"
v-model="auth.grantTypeInfo.token"
:environment-highlights="false"
placeholder="Token"
/>
</div>
<HttpOAuth2Authorization v-model="auth" />
<HttpOAuth2Authorization v-model="auth" source="GraphQL" />
</div>
<div v-if="auth.authType === 'api-key'">
<HttpAuthorizationApiKey v-model="auth" />
@@ -220,19 +220,22 @@
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { pluckRef } from "@composables/ref"
import { useColorMode } from "@composables/theming"
import { HoppGQLAuth, HoppGQLAuthOAuth2 } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { computed, onMounted, ref } from "vue"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import IconCircle from "~icons/lucide/circle"
import IconCircleDot from "~icons/lucide/circle-dot"
import IconExternalLink from "~icons/lucide/external-link"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconExternalLink from "~icons/lucide/external-link"
import IconCircleDot from "~icons/lucide/circle-dot"
import IconCircle from "~icons/lucide/circle"
import { computed, ref } from "vue"
import { HoppGQLAuth } from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { onMounted } from "vue"
import { getDefaultAuthCodeOauthFlowParams } from "~/services/oauth/flows/authCode"
const t = useI18n()
@@ -280,6 +283,30 @@ const getAuthName = (type: HoppGQLAuth["authType"] | undefined) => {
return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None"
}
const selectOAuth2AuthType = () => {
const defaultGrantTypeInfo: HoppGQLAuthOAuth2["grantTypeInfo"] = {
...getDefaultAuthCodeOauthFlowParams(),
grantType: "AUTHORIZATION_CODE",
token: "",
}
// @ts-expect-error - the existing grantTypeInfo might be in the auth object, typescript doesnt know that
const existingGrantTypeInfo = auth.value.grantTypeInfo as
| HoppGQLAuthOAuth2["grantTypeInfo"]
| undefined
const grantTypeInfo = existingGrantTypeInfo
? existingGrantTypeInfo
: defaultGrantTypeInfo
auth.value = <HoppGQLAuth>{
...auth.value,
authType: "oauth-2",
addTo: "HEADERS",
grantTypeInfo: grantTypeInfo,
}
}
const authActive = pluckRef(auth, "authActive")
const clearContent = () => {

View File

@@ -579,12 +579,18 @@ const getComputedAuthHeaders = (
})
} else if (
request.auth.authType === "bearer" ||
request.auth.authType === "oauth-2"
(request.auth.authType === "oauth-2" && request.auth.addTo === "HEADERS")
) {
const requestAuth = request.auth
const isOAuth2 = requestAuth.authType === "oauth-2"
const token = isOAuth2 ? requestAuth.grantTypeInfo.token : requestAuth.token
headers.push({
active: true,
key: "Authorization",
value: `Bearer ${request.auth.token}`,
value: `Bearer ${token}`,
})
} else if (request.auth.authType === "api-key") {
const { key, addTo } = request.auth

View File

@@ -82,7 +82,7 @@
:active="authName === 'OAuth 2.0'"
@click="
() => {
auth.authType = 'oauth-2'
selectOAuth2AuthType()
hide()
}
"
@@ -177,15 +177,24 @@
/>
</div>
</div>
<div v-if="auth.authType === 'oauth-2'">
<div v-if="auth.authType === 'oauth-2'" class="w-full">
<div class="flex flex-1 border-b border-dividerLight">
<!-- Ensure a new object is assigned here to avoid reactivity issues -->
<SmartEnvInput
v-model="auth.token"
:model-value="auth.grantTypeInfo.token"
placeholder="Token"
:envs="envs"
@update:model-value="
auth.grantTypeInfo = { ...auth.grantTypeInfo, token: $event }
"
/>
</div>
<HttpOAuth2Authorization v-model="auth" :envs="envs" />
<HttpOAuth2Authorization
v-model="auth"
:is-collection-property="isCollectionProperty"
:envs="envs"
:source="source"
/>
</div>
<div v-if="auth.authType === 'api-key'">
<HttpAuthorizationApiKey v-model="auth" :envs="envs" />
@@ -217,7 +226,7 @@ import IconExternalLink from "~icons/lucide/external-link"
import IconCircleDot from "~icons/lucide/circle-dot"
import IconCircle from "~icons/lucide/circle"
import { computed, ref } from "vue"
import { HoppRESTAuth } from "@hoppscotch/data"
import { HoppRESTAuth, HoppRESTAuthOAuth2 } from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
@@ -226,17 +235,27 @@ import { onMounted } from "vue"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { AggregateEnvironment } from "~/newstore/environments"
import { getDefaultAuthCodeOauthFlowParams } from "~/services/oauth/flows/authCode"
const t = useI18n()
const colorMode = useColorMode()
const props = defineProps<{
modelValue: HoppRESTAuth
isCollectionProperty?: boolean
isRootCollection?: boolean
inheritedProperties?: HoppInheritedProperty
envs?: AggregateEnvironment[]
}>()
const props = withDefaults(
defineProps<{
modelValue: HoppRESTAuth
isCollectionProperty?: boolean
isRootCollection?: boolean
inheritedProperties?: HoppInheritedProperty
envs?: AggregateEnvironment[]
source?: "REST" | "GraphQL"
}>(),
{
source: "REST",
envs: undefined,
inheritedProperties: undefined,
}
)
const emit = defineEmits<{
(e: "update:modelValue", value: HoppRESTAuth): void
@@ -272,6 +291,30 @@ const getAuthName = (type: HoppRESTAuth["authType"] | undefined) => {
return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None"
}
const selectOAuth2AuthType = () => {
const defaultGrantTypeInfo: HoppRESTAuthOAuth2["grantTypeInfo"] = {
...getDefaultAuthCodeOauthFlowParams(),
grantType: "AUTHORIZATION_CODE",
token: "",
}
// @ts-expect-error - the existing grantTypeInfo might be in the auth object, typescript doesnt know that
const existingGrantTypeInfo = auth.value.grantTypeInfo as
| HoppRESTAuthOAuth2["grantTypeInfo"]
| undefined
const grantTypeInfo = existingGrantTypeInfo
? existingGrantTypeInfo
: defaultGrantTypeInfo
auth.value = <HoppRESTAuth>{
...auth.value,
authType: "oauth-2",
addTo: "HEADERS",
grantTypeInfo: grantTypeInfo,
}
}
const authActive = pluckRef(auth, "authActive")
const clearContent = () => {