diff --git a/packages/hoppscotch-cli/src/__tests__/unit/fixtures/workspace-access.mock.ts b/packages/hoppscotch-cli/src/__tests__/unit/fixtures/workspace-access.mock.ts index acb7fd61..3fc497a7 100644 --- a/packages/hoppscotch-cli/src/__tests__/unit/fixtures/workspace-access.mock.ts +++ b/packages/hoppscotch-cli/src/__tests__/unit/fixtures/workspace-access.mock.ts @@ -485,17 +485,17 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppCollection[] = [ { - v: 8, + v: 9, id: "clx1f86hv000010f8szcfya0t", name: "Multiple child collections with authorization & headers set at each level", folders: [ { - v: 8, + v: 9, id: "clx1fjgah000110f8a5bs68gd", name: "folder-1", folders: [ { - v: 8, + v: 9, id: "clx1fjwmm000410f8l1gkkr1a", name: "folder-11", folders: [], @@ -537,7 +537,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp ], }, { - v: 8, + v: 9, id: "clx1fjyxm000510f8pv90dt43", name: "folder-12", folders: [], @@ -595,7 +595,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp ], }, { - v: 8, + v: 9, id: "clx1fk1cv000610f88kc3aupy", name: "folder-13", folders: [], @@ -707,12 +707,12 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp ], }, { - v: 8, + v: 9, id: "clx1fjk9o000210f8j0573pls", name: "folder-2", folders: [ { - v: 8, + v: 9, id: "clx1fk516000710f87sfpw6bo", name: "folder-21", folders: [], @@ -752,7 +752,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp ], }, { - v: 8, + v: 9, id: "clx1fk72t000810f8gfwkpi5y", name: "folder-22", folders: [], @@ -810,7 +810,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp ], }, { - v: 8, + v: 9, id: "clx1fk95g000910f8bunhaoo8", name: "folder-23", folders: [], @@ -915,12 +915,12 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp ], }, { - v: 8, + v: 9, id: "clx1fjmlq000310f86o4d3w2o", name: "folder-3", folders: [ { - v: 8, + v: 9, id: "clx1iwq0p003e10f8u8zg0p85", name: "folder-31", folders: [], @@ -960,7 +960,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp ], }, { - v: 8, + v: 9, id: "clx1izut7003m10f894ip59zg", name: "folder-32", folders: [], @@ -1018,7 +1018,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp ], }, { - v: 8, + v: 9, id: "clx1j2ka9003q10f8cdbzpgpg", name: "folder-33", folders: [], diff --git a/packages/hoppscotch-cli/src/utils/workspace-access.ts b/packages/hoppscotch-cli/src/utils/workspace-access.ts index 2ab6c7ac..2c408803 100644 --- a/packages/hoppscotch-cli/src/utils/workspace-access.ts +++ b/packages/hoppscotch-cli/src/utils/workspace-access.ts @@ -76,6 +76,71 @@ const normalizeEnvironmentVariable = (variable: HoppEnvPair): HoppEnvPair => { }; }; +/** + * Transforms the given `HoppRESTAuth` object to ensure it conforms to the latest + * OAuth 2.0 authentication structure. Depending on the `grantType` within the + * `grantTypeInfo` property, this function adds or initializes specific fields + * such as `clientAuthentication`, `authRequestParams`, `tokenRequestParams`, + * and `refreshRequestParams` to maintain compatibility with updated schema + * requirements. + * + * - For "CLIENT_CREDENTIALS" grant type, sets `clientAuthentication` to "IN_BODY" + * and initializes `tokenRequestParams` and `refreshRequestParams` as empty arrays. + * - For "AUTHORIZATION_CODE" grant type, initializes `authRequestParams`, + * `tokenRequestParams`, and `refreshRequestParams` as empty arrays. + * - For "PASSWORD" grant type, initializes `tokenRequestParams` and + * `refreshRequestParams` as empty arrays. + * - For "IMPLICIT" grant type, initializes `authRequestParams` and + * `refreshRequestParams` as empty arrays. + * + * If the `authType` is not "oauth-2", the original `auth` object is returned unchanged. + * + * @param {HoppRESTAuth} auth - The authentication object to transform. + * @returns {HoppRESTAuth} The transformed authentication object with updated grant type information. + */ +const transformAuth = (auth: HoppRESTAuth): HoppRESTAuth => { + if (auth.authType === "oauth-2") { + const oldGrantTypeInfo = auth.grantTypeInfo; + let newGrantTypeInfo = oldGrantTypeInfo; + + // Add clientAuthentication for CLIENT_CREDENTIALS + if (oldGrantTypeInfo.grantType === "CLIENT_CREDENTIALS") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + clientAuthentication: "IN_BODY", + tokenRequestParams: [], + refreshRequestParams: [], + }; + } else if (oldGrantTypeInfo.grantType === "AUTHORIZATION_CODE") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + authRequestParams: [], + tokenRequestParams: [], + refreshRequestParams: [], + }; + } else if (oldGrantTypeInfo.grantType === "PASSWORD") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + tokenRequestParams: [], + refreshRequestParams: [], + }; + } else if (oldGrantTypeInfo.grantType === "IMPLICIT") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + authRequestParams: [], + refreshRequestParams: [], + }; + } + + return { + ...auth, + grantTypeInfo: newGrantTypeInfo, + }; + } + + return auth; +}; + /** * Transforms workspace environment data to the `HoppEnvironment` format. * @@ -115,21 +180,9 @@ export const transformWorkspaceCollections = ( const { auth = { authType: "inherit", authActive: true }, headers = [] } = parsedData; - const migratedAuth: HoppRESTAuth = - auth.authType === "oauth-2" - ? { - ...auth, - grantTypeInfo: - auth.grantTypeInfo.grantType === "CLIENT_CREDENTIALS" - ? { - ...auth.grantTypeInfo, - clientAuthentication: "IN_BODY", - } - : auth.grantTypeInfo, - } - : auth; + const transformedAuth = transformAuth(auth); - const migratedHeaders = headers.map((header) => + const transformedHeaders = headers.map((header) => header.description ? header : { ...header, description: "" } ); @@ -142,8 +195,8 @@ export const transformWorkspaceCollections = ( name: title, folders: transformWorkspaceCollections(folders), requests: transformWorkspaceRequests(requests), - auth: migratedAuth, - headers: migratedHeaders, + auth: transformedAuth, + headers: transformedHeaders, }; }); }; diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index ff27fdec..ae0e76bb 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -241,7 +241,11 @@ "label_send_as": "Client Authentication", "label_send_in_body": "Send Credentials in Body", "label_send_as_basic_auth": "Send Credentials as Basic Auth", - "enter_value": "Enter value" + "enter_value": "Enter value", + "auth_request": "Auth Request", + "token_request": "Token Request", + "refresh_request": "Refresh Request", + "send_in": "Send In" }, "pass_key_by": "Pass by", "pass_by_query_params_label": "Query Parameters", diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts index 22747b90..3392c2b4 100644 --- a/packages/hoppscotch-common/src/components.d.ts +++ b/packages/hoppscotch-common/src/components.d.ts @@ -211,7 +211,9 @@ declare module 'vue' { IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default'] IconLucideBrush: typeof import('~icons/lucide/brush')['default'] IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default'] + IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default'] IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default'] + IconLucideChevronUp: typeof import('~icons/lucide/chevron-up')['default'] IconLucideCircleCheck: typeof import('~icons/lucide/circle-check')['default'] IconLucideGlobe: typeof import('~icons/lucide/globe')['default'] IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default'] @@ -221,7 +223,6 @@ declare module 'vue' { IconLucideListEnd: typeof import('~icons/lucide/list-end')['default'] IconLucideMinus: typeof import('~icons/lucide/minus')['default'] IconLucidePlusCircle: typeof import('~icons/lucide/plus-circle')['default'] - IconLucideRss: typeof import('~icons/lucide/rss')['default'] IconLucideSearch: typeof import('~icons/lucide/search')['default'] IconLucideTriangleAlert: typeof import('~icons/lucide/triangle-alert')['default'] IconLucideUsers: typeof import('~icons/lucide/users')['default'] @@ -254,6 +255,7 @@ declare module 'vue' { LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default'] LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default'] LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default'] + MonacoScriptEditor: typeof import('./components/MonacoScriptEditor.vue')['default'] ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default'] RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default'] RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default'] diff --git a/packages/hoppscotch-common/src/components/http/KeyValue.vue b/packages/hoppscotch-common/src/components/http/KeyValue.vue index f2200648..e8e5622c 100644 --- a/packages/hoppscotch-common/src/components/http/KeyValue.vue +++ b/packages/hoppscotch-common/src/components/http/KeyValue.vue @@ -23,7 +23,7 @@ :model-value="name" :placeholder="t('count.key')" :auto-complete-source="keyAutoCompleteSource" - :auto-complete-env="true" + :auto-complete-env="autoCompleteEnv" :envs="envs" :inspection-results="inspectionKeyResult" @update:model-value="emit('update:name', $event)" @@ -41,7 +41,7 @@ :class="{ 'opacity-50': !entityActive }" :model-value="value" :placeholder="t('count.value')" - :auto-complete-env="true" + :auto-complete-env="autoCompleteEnv" :envs="envs" :inspection-results="inspectionValueResult" @update:model-value="emit('update:value', $event)" @@ -56,7 +56,10 @@ " /> + + () +withDefaults( + defineProps<{ + showDescription?: boolean + total: number + index: number + entityId: number + isActive: boolean + entityActive: boolean + name: string + value: string + inspectionKeyResult?: InspectorResult[] + inspectionValueResult?: InspectorResult[] + description?: string + envs?: AggregateEnvironment[] + autoCompleteEnv?: boolean + keyAutoCompleteSource?: string[] + }>(), + { + showDescription: true, + description: "", + inspectionKeyResult: () => [], + inspectionValueResult: () => [], + envs: () => [], + autoCompleteEnv: true, + keyAutoCompleteSource: () => [], + } +) const emit = defineEmits<{ (e: "update:name", value: string): void diff --git a/packages/hoppscotch-common/src/components/http/authorization/OAuth2.vue b/packages/hoppscotch-common/src/components/http/authorization/OAuth2.vue index d0d96fab..8f1c2f55 100644 --- a/packages/hoppscotch-common/src/components/http/authorization/OAuth2.vue +++ b/packages/hoppscotch-common/src/components/http/authorization/OAuth2.vue @@ -172,6 +172,364 @@ +
+
+ {{ t("authorization.advance_config") }} + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + {{ t("count.key") }} + + + {{ t("count.value") }} + + + + + +
+ +
+ + {{ t("empty.parameters") }} + + + +
+ + +
+ +
+
+
+
+ +
+ +
+
+ +
+ +
+ +
+ + + + {{ t("count.key") }} + + + {{ t("count.value") }} + + + {{ t("authorization.oauth.send_in") }} + + + + + +
+ +
+ + {{ t("empty.parameters") }} + + + +
+ + +
+ + + +
+
+
+
+ +
+ +
+
+ +
+ +
+ +
+ + + + {{ t("count.key") }} + + + {{ t("count.value") }} + + + {{ t("authorization.oauth.send_in") }} + + + + + +
+ +
+ + {{ t("empty.parameters") }} + + + +
+ + +
+ + + +
+
+
+
+
+
+
p.active && p.key && p.value) + .map((p) => ({ + id: p.id, + key: replaceTemplateString(p.key), + value: replaceTemplateString(p.value), + active: p.active, + sendIn: p.sendIn, + })), + tokenRequestParams: workingTokenRequestParams.value + .filter((p) => p.active && p.key && p.value) + .map((p) => ({ + id: p.id, + key: replaceTemplateString(p.key), + value: replaceTemplateString(p.value), + active: p.active, + sendIn: p.sendIn, + })), + refreshRequestParams: workingRefreshRequestParams.value + .filter((p) => p.active && p.key && p.value) + .map((p) => ({ + id: p.id, + key: replaceTemplateString(p.key), + value: replaceTemplateString(p.value), + active: p.active, + sendIn: p.sendIn, + })), } const unwrappedParams = replaceTemplateStringsInObjectValues(params) @@ -998,7 +1395,7 @@ const setAccessTokenInActiveContext = ( tabService.currentActiveTab.value.document.request.auth.authType === "oauth-2" ) { - // @ts-expect-error - todo: narrow the grantType to only supporting refresh tokens + // @ts-expect-error - TODO: narrow the grantType to only supporting refresh tokens tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.refreshToken = refreshToken } @@ -1133,4 +1530,305 @@ const grantTypeTippyActions = ref(null) const pkceTippyActions = ref(null) const authTippyActions = ref(null) const clientAuthenticationTippyActions = ref(null) + +// Advanced Configuration state +const isAdvancedConfigExpanded = ref(false) + +const toggleAdvancedConfig = () => { + isAdvancedConfigExpanded.value = !isAdvancedConfigExpanded.value +} + +// Advanced Configuration: Auth Request Parameters +type OAuth2AdvancedParam = { + id: number + key: string + value: string + active: boolean + sendIn?: "headers" | "url" | "body" +} + +let paramsIdCounter = 1000 + +// Initialize working auth request params +const workingAuthRequestParams = ref([ + { id: paramsIdCounter++, key: "", value: "", active: true }, +]) + +// Watch for changes in working auth request params +watch( + workingAuthRequestParams, + (newParams: OAuth2AdvancedParam[]) => { + // Auto-add empty row when the last row is filled + if (newParams.length > 0 && newParams[newParams.length - 1].key !== "") { + workingAuthRequestParams.value.push({ + id: paramsIdCounter++, + key: "", + value: "", + active: true, + }) + } + + // Update auth.value.grantTypeInfo with non-empty params + const nonEmptyParams = newParams.filter( + (p: OAuth2AdvancedParam) => p.key !== "" || p.value !== "" + ) + + if ("authRequestParams" in auth.value.grantTypeInfo) { + auth.value.grantTypeInfo.authRequestParams = nonEmptyParams.map( + (param) => ({ + id: param.id, + key: param.key, + value: param.value, + active: param.active, + }) + ) + } + }, + { deep: true } +) + +// Functions for auth request params management +const addAuthRequestParam = () => { + workingAuthRequestParams.value.push({ + id: paramsIdCounter++, + key: "", + value: "", + active: true, + }) +} + +const updateAuthRequestParam = ( + index: number, + payload: OAuth2AdvancedParam +) => { + workingAuthRequestParams.value[index] = payload +} + +const deleteAuthRequestParam = (index: number) => { + // Only delete if it's not the last empty row, or if there are multiple rows + if (workingAuthRequestParams.value.length > 1) { + workingAuthRequestParams.value.splice(index, 1) + } +} + +// Token Request Parameters +interface OAuth2TokenParam { + id: number + key: string + value: string + sendIn: "headers" | "body" | "url" + active: boolean +} + +// Initialize working token request params +const workingTokenRequestParams = ref([ + { + id: paramsIdCounter++, + key: "", + value: "", + sendIn: "body", + active: true, + }, +]) + +// Watch for changes in working token request params +watch( + workingTokenRequestParams, + (newParams: OAuth2TokenParam[]) => { + // Auto-add empty row when the last row is filled + if (newParams.length > 0 && newParams[newParams.length - 1].key !== "") { + workingTokenRequestParams.value.push({ + id: paramsIdCounter++, + key: "", + value: "", + sendIn: "body", + active: true, + }) + } + + // Update auth.value.grantTypeInfo with non-empty params + const nonEmptyParams = newParams.filter( + (p: OAuth2TokenParam) => p.key !== "" || p.value !== "" + ) + + if ("tokenRequestParams" in auth.value.grantTypeInfo) { + auth.value.grantTypeInfo.tokenRequestParams = nonEmptyParams.map( + (param) => ({ + id: param.id, + key: param.key, + value: param.value, + sendIn: param.sendIn, + active: param.active, + }) + ) + } + }, + { deep: true } +) + +// Functions for token request params management +const addTokenRequestParam = () => { + workingTokenRequestParams.value.push({ + id: paramsIdCounter++, + key: "", + value: "", + sendIn: "body", + active: true, + }) +} + +const updateTokenRequestParam = (index: number, payload: OAuth2TokenParam) => { + workingTokenRequestParams.value[index] = payload +} + +const deleteTokenRequestParam = (index: number) => { + // Only delete if it's not the last empty row, or if there are multiple rows + if (workingTokenRequestParams.value.length > 1) { + workingTokenRequestParams.value.splice(index, 1) + } +} + +// Refresh Request Parameters +interface OAuth2RefreshParam { + id: number + key: string + value: string + sendIn: "headers" | "body" | "url" + active: boolean +} + +// Initialize working refresh request params +const workingRefreshRequestParams = ref([ + { + id: paramsIdCounter++, + key: "", + value: "", + sendIn: "body", + active: true, + }, +]) + +// Watch for changes in working refresh request params +watch( + workingRefreshRequestParams, + (newParams: OAuth2RefreshParam[]) => { + // Auto-add empty row when the last row is filled + if (newParams.length > 0 && newParams[newParams.length - 1].key !== "") { + workingRefreshRequestParams.value.push({ + id: paramsIdCounter++, + key: "", + value: "", + sendIn: "body", + active: true, + }) + } + + // Update auth.value.grantTypeInfo with non-empty params + const nonEmptyParams = newParams.filter( + (p: OAuth2RefreshParam) => p.key !== "" || p.value !== "" + ) + + if ("refreshRequestParams" in auth.value.grantTypeInfo) { + auth.value.grantTypeInfo.refreshRequestParams = nonEmptyParams.map( + (param) => ({ + id: param.id, + key: param.key, + value: param.value, + sendIn: param.sendIn, + active: param.active, + }) + ) + } + }, + { deep: true } +) + +// Functions for refresh request params management +const addRefreshRequestParam = () => { + workingRefreshRequestParams.value.push({ + id: paramsIdCounter++, + key: "", + value: "", + sendIn: "body", + active: true, + }) +} + +const updateRefreshRequestParam = ( + index: number, + payload: OAuth2RefreshParam +) => { + workingRefreshRequestParams.value[index] = payload +} + +const deleteRefreshRequestParam = (index: number) => { + // Only delete if it's not the last empty row, or if there are multiple rows + if (workingRefreshRequestParams.value.length > 1) { + workingRefreshRequestParams.value.splice(index, 1) + } +} + +// Initialize advanced parameters from the auth object when component mounts +onMounted(() => { + if ( + "authRequestParams" in auth.value.grantTypeInfo && + auth.value.grantTypeInfo.authRequestParams && + auth.value.grantTypeInfo.authRequestParams.length > 0 + ) { + workingAuthRequestParams.value = + auth.value.grantTypeInfo.authRequestParams.map((param) => ({ + id: param.id || paramsIdCounter++, + key: param.key, + value: param.value, + active: param.active, + })) + } + + if ( + "tokenRequestParams" in auth.value.grantTypeInfo && + auth.value.grantTypeInfo.tokenRequestParams && + auth.value.grantTypeInfo.tokenRequestParams.length > 0 + ) { + workingTokenRequestParams.value = [ + ...auth.value.grantTypeInfo.tokenRequestParams.map((param) => ({ + id: param.id || paramsIdCounter++, + key: param.key, + value: param.value, + sendIn: param.sendIn || "body", + active: param.active, + })), + + { + id: paramsIdCounter++, + key: "", + value: "", + sendIn: "body", + active: true, + }, + ] + } + + if ( + "refreshRequestParams" in auth.value.grantTypeInfo && + auth.value.grantTypeInfo.refreshRequestParams && + auth.value.grantTypeInfo.refreshRequestParams.length > 0 + ) { + workingRefreshRequestParams.value = [ + ...auth.value.grantTypeInfo.refreshRequestParams.map((param) => ({ + id: param.id || paramsIdCounter++, + key: param.key, + value: param.value, + sendIn: param.sendIn || "body", + active: param.active, + })), + { + id: paramsIdCounter++, + key: "", + value: "", + sendIn: "body", + active: true, + }, + ] + } +}) diff --git a/packages/hoppscotch-common/src/helpers/auth/index.ts b/packages/hoppscotch-common/src/helpers/auth/index.ts index 554711eb..f3402385 100644 --- a/packages/hoppscotch-common/src/helpers/auth/index.ts +++ b/packages/hoppscotch-common/src/helpers/auth/index.ts @@ -56,3 +56,7 @@ export const replaceTemplateStringsInObjectValues = < return newObj as T } + +export const replaceTemplateString = (str: string): string => { + return replaceTemplateStringsInObjectValues({ value: str }).value +} diff --git a/packages/hoppscotch-common/src/helpers/graphql/connection.ts b/packages/hoppscotch-common/src/helpers/graphql/connection.ts index b5036d2d..2e97c8ec 100644 --- a/packages/hoppscotch-common/src/helpers/graphql/connection.ts +++ b/packages/hoppscotch-common/src/helpers/graphql/connection.ts @@ -401,7 +401,7 @@ export const runGQLOperation = async (options: RunQueryOptions) => { .forEach(({ key, value }) => (finalHeaders[key] = value)) const gqlRequest: HoppGQLRequest = { - v: 8, + v: 9, name: options.name || "Untitled Request", url: finalUrl, headers: request.headers, diff --git a/packages/hoppscotch-common/src/helpers/kernel/__tests__/kernel.spec.ts b/packages/hoppscotch-common/src/helpers/kernel/__tests__/kernel.spec.ts index de00eec4..3e3c1095 100644 --- a/packages/hoppscotch-common/src/helpers/kernel/__tests__/kernel.spec.ts +++ b/packages/hoppscotch-common/src/helpers/kernel/__tests__/kernel.spec.ts @@ -136,7 +136,7 @@ describe("GraphQL Response Transformation", () => { describe("GraphQL Request Transformation", () => { const baseRequest: HoppGQLRequest = { - v: 8, + v: 9, name: "Test Query", url: "https://api.example.com/graphql", headers: [], @@ -235,7 +235,7 @@ describe("REST Response Transformation", () => { } const originalRequest: HoppRESTRequest = { - v: "11", + v: "15", endpoint: "https://api.example.com", name: "Test Request", method: "GET", @@ -244,7 +244,7 @@ describe("REST Response Transformation", () => { preRequestScript: "", testScript: "", auth: { authType: "none", authActive: true }, - body: { contentmediaType: null, body: null }, + body: { contentType: null, body: null }, requestVariables: [], responses: {}, } @@ -271,7 +271,7 @@ describe("REST Response Transformation", () => { } const originalRequest: HoppRESTRequest = { - v: "11", + v: "15", endpoint: "https://api.example.com", name: "Test Request", method: "GET", @@ -280,7 +280,7 @@ describe("REST Response Transformation", () => { preRequestScript: "", testScript: "", auth: { authType: "none", authActive: true }, - body: { contentmediaType: null, body: null }, + body: { contentType: null, body: null }, requestVariables: [], responses: {}, } @@ -300,7 +300,7 @@ describe("REST Response Transformation", () => { describe("REST Request Transformation", () => { const baseEffectiveRequest: EffectiveHoppRESTRequest = { - v: "11", + v: "15", name: "Test Request", method: "GET", endpoint: "https://api.example.com", @@ -310,7 +310,7 @@ describe("REST Request Transformation", () => { preRequestScript: "", testScript: "", auth: { authType: "none", authActive: true }, - body: { contentmediaType: null, body: null }, + body: { contentType: null, body: null }, requestVariables: [], responses: {}, effectiveFinalHeaders: [], diff --git a/packages/hoppscotch-common/src/helpers/oauth2Params.ts b/packages/hoppscotch-common/src/helpers/oauth2Params.ts new file mode 100644 index 00000000..ca5e6801 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/oauth2Params.ts @@ -0,0 +1,59 @@ +export const commonOAuth2AuthParams = [ + "audience", + "scope", + "state", + "nonce", + "prompt", + "max_age", + "ui_locales", + "id_token_hint", + "login_hint", + "acr_values", + "response_mode", + "display", + "claims", + "request", + "request_uri", +] + +export const commonOAuth2TokenParams = [ + "grant_type", + "code", + "redirect_uri", + "client_id", + "client_secret", + "code_verifier", + "username", + "password", + "scope", + "audience", + "resource", + "assertion", + "assertion_type", + "refresh_token", +] + +export const commonOAuth2RefreshParams = [ + "grant_type", + "refresh_token", + "client_id", + "client_secret", + "scope", + "audience", + "resource", +] + +export const sendInOptions = [ + { + label: "Request Body", + value: "body", + }, + { + label: "Request URL", + value: "url", + }, + { + label: "Request Headers", + value: "headers", + }, +] diff --git a/packages/hoppscotch-common/src/services/oauth/flows/authCode.ts b/packages/hoppscotch-common/src/services/oauth/flows/authCode.ts index be2e182e..0a792fb0 100644 --- a/packages/hoppscotch-common/src/services/oauth/flows/authCode.ts +++ b/packages/hoppscotch-common/src/services/oauth/flows/authCode.ts @@ -10,21 +10,47 @@ import { z } from "zod" import { getService } from "~/modules/dioc" import * as E from "fp-ts/Either" import { KernelInterceptorService } from "~/services/kernel-interceptor.service" -import { AuthCodeGrantTypeParams } from "@hoppscotch/data" import { content } from "@hoppscotch/kernel" const persistenceService = getService(PersistenceService) const interceptorService = getService(KernelInterceptorService) -const AuthCodeOauthFlowParamsSchema = AuthCodeGrantTypeParams.pick({ - authEndpoint: true, - tokenEndpoint: true, - clientID: true, - clientSecret: true, - scopes: true, - isPKCE: true, - codeVerifierMethod: true, -}) +const AuthCodeOauthFlowParamsSchema = z + .object({ + authEndpoint: z.string(), + tokenEndpoint: z.string(), + clientID: z.string(), + clientSecret: z.string().optional(), + scopes: z.string().optional(), + isPKCE: z.boolean(), + codeVerifierMethod: z.enum(["plain", "S256"]).optional(), + authRequestParams: z.array( + z.object({ + id: z.number(), + key: z.string(), + value: z.string(), + active: z.boolean(), + }) + ), + refreshRequestParams: z.array( + z.object({ + id: z.number(), + key: z.string(), + value: z.string(), + active: z.boolean(), + sendIn: z.enum(["headers", "url", "body"]).optional(), + }) + ), + tokenRequestParams: z.array( + z.object({ + id: z.number(), + key: z.string(), + value: z.string(), + active: z.boolean(), + sendIn: z.enum(["headers", "url", "body"]).optional(), + }) + ), + }) .refine( (params) => { return ( @@ -63,6 +89,9 @@ export const getDefaultAuthCodeOauthFlowParams = scopes: undefined, isPKCE: false, codeVerifierMethod: "S256", + authRequestParams: [], + refreshRequestParams: [], + tokenRequestParams: [], }) const initAuthCodeOauthFlow = async ({ @@ -73,6 +102,9 @@ const initAuthCodeOauthFlow = async ({ authEndpoint, isPKCE, codeVerifierMethod, + authRequestParams, + refreshRequestParams, + tokenRequestParams, }: AuthCodeOauthFlowParams) => { const state = generateRandomString() @@ -99,6 +131,24 @@ const initAuthCodeOauthFlow = async ({ codeVerifierMethod?: string codeChallenge?: string scopes?: string + authRequestParams?: Array<{ + key: string + value: string + active: boolean + sendIn?: string + }> + refreshRequestParams?: Array<{ + key: string + value: string + active: boolean + sendIn?: string + }> + tokenRequestParams?: Array<{ + key: string + value: string + active: boolean + sendIn?: string + }> } = { state, grant_type: "AUTHORIZATION_CODE", @@ -109,6 +159,9 @@ const initAuthCodeOauthFlow = async ({ isPKCE, codeVerifierMethod, scopes, + authRequestParams, + refreshRequestParams, + tokenRequestParams, } if (codeVerifier && codeChallenge) { @@ -160,6 +213,14 @@ const initAuthCodeOauthFlow = async ({ url.searchParams.set("code_challenge_method", codeVerifierMethod) } + if (authRequestParams.length > 0) { + authRequestParams.forEach((param) => { + if (param.active && param.key && param.value) { + url.searchParams.set(param.key, param.value) + } + }) + } + // Redirect to the authorization server window.location.assign(url.toString()) diff --git a/packages/hoppscotch-common/src/services/persistence/__tests__/__mocks__/index.ts b/packages/hoppscotch-common/src/services/persistence/__tests__/__mocks__/index.ts index 66334fff..b24c8ab1 100644 --- a/packages/hoppscotch-common/src/services/persistence/__tests__/__mocks__/index.ts +++ b/packages/hoppscotch-common/src/services/persistence/__tests__/__mocks__/index.ts @@ -25,7 +25,7 @@ const DEFAULT_SETTINGS = getDefaultSettings() export const REST_COLLECTIONS_MOCK: HoppCollection[] = [ { - v: 8, + v: 9, name: "Echo", requests: [ { @@ -57,11 +57,11 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [ export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [ { - v: 8, + v: 9, name: "Echo", requests: [ { - v: 8, + v: 9, name: "Echo test", url: "https://echo.hoppscotch.io/graphql", headers: [], @@ -182,7 +182,7 @@ export const GQL_HISTORY_MOCK: GQLHistoryEntry[] = [ { v: 1, request: { - v: 8, + v: 9, name: "Untitled", url: "https://echo.hoppscotch.io/graphql", query: "query Request { url }", @@ -203,7 +203,7 @@ export const GQL_TAB_STATE_MOCK: PersistableTabState = { tabID: "5edbe8d4-65c9-4381-9354-5f1bf05d8ccc", doc: { request: { - v: 8, + v: 9, name: "Untitled", url: "https://echo.hoppscotch.io/graphql", headers: [], diff --git a/packages/hoppscotch-data/src/collection/index.ts b/packages/hoppscotch-data/src/collection/index.ts index 5f9a009c..5542d51c 100644 --- a/packages/hoppscotch-data/src/collection/index.ts +++ b/packages/hoppscotch-data/src/collection/index.ts @@ -8,6 +8,7 @@ import V5_VERSION from "./v/5" import V6_VERSION from "./v/6" import V7_VERSION from "./v/7" import V8_VERSION from "./v/8" +import V9_VERSION from "./v/9" import { z } from "zod" import { translateToNewRequest } from "../rest" @@ -19,7 +20,7 @@ const versionedObject = z.object({ }) export const HoppCollection = createVersionedEntity({ - latestVersion: 8, + latestVersion: 9, versionMap: { 1: V1_VERSION, 2: V2_VERSION, @@ -29,6 +30,7 @@ export const HoppCollection = createVersionedEntity({ 6: V6_VERSION, 7: V7_VERSION, 8: V8_VERSION, + 9: V9_VERSION, }, getVersion(data) { const versionCheck = versionedObject.safeParse(data) @@ -44,7 +46,7 @@ export const HoppCollection = createVersionedEntity({ export type HoppCollection = InferredEntity -export const CollectionSchemaVersion = 8 +export const CollectionSchemaVersion = 9 /** * Generates a Collection object. This ignores the version number object diff --git a/packages/hoppscotch-data/src/collection/v/9.ts b/packages/hoppscotch-data/src/collection/v/9.ts new file mode 100644 index 00000000..7fea6afa --- /dev/null +++ b/packages/hoppscotch-data/src/collection/v/9.ts @@ -0,0 +1,92 @@ +import { defineVersion, entityRefUptoVersion } from "verzod" +import { z } from "zod" + +import { HoppGQLAuth } from "../../graphql/v/9" +import { HoppRESTAuth } from "../../rest/v/15/auth" + +import { HoppCollection } from ".." +import { v8_baseCollectionSchema, V8_SCHEMA } from "./8" + +export const v9_baseCollectionSchema = v8_baseCollectionSchema.extend({ + v: z.literal(9), + auth: z.union([HoppRESTAuth, HoppGQLAuth]), +}) + +type Input = z.input & { + folders: Input[] +} + +type Output = z.output & { + folders: Output[] +} + +export const V9_SCHEMA = v9_baseCollectionSchema.extend({ + folders: z.lazy(() => z.array(entityRefUptoVersion(HoppCollection, 9))), +}) as z.ZodType + +export default defineVersion({ + initial: false, + schema: V9_SCHEMA, + up(old: z.infer) { + // Migrate auth field if it's OAuth2 to include new advanced parameters + let newAuth: z.infer["auth"] + if (old.auth.authType === "oauth-2") { + const oldGrantTypeInfo = old.auth.grantTypeInfo + let newGrantTypeInfo + + // Add the advanced parameters to the appropriate grant type + if (oldGrantTypeInfo.grantType === "AUTHORIZATION_CODE") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + authRequestParams: [], + tokenRequestParams: [], + refreshRequestParams: [], + } + } else if (oldGrantTypeInfo.grantType === "CLIENT_CREDENTIALS") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + tokenRequestParams: [], + refreshRequestParams: [], + } + } else if (oldGrantTypeInfo.grantType === "PASSWORD") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + tokenRequestParams: [], + refreshRequestParams: [], + } + } else if (oldGrantTypeInfo.grantType === "IMPLICIT") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + authRequestParams: [], + refreshRequestParams: [], + } + } else { + newGrantTypeInfo = oldGrantTypeInfo + } + + newAuth = { + ...old.auth, + grantTypeInfo: newGrantTypeInfo, + } as z.infer["auth"] + } else { + newAuth = old.auth + } + + const result: z.infer = { + ...old, + v: 9 as const, + auth: newAuth, + folders: old.folders.map((folder) => { + const result = HoppCollection.safeParseUpToVersion(folder, 9) + + if (result.type !== "ok") { + throw new Error("Failed to migrate child collections") + } + + return result.value + }), + } + + return result + }, +}) diff --git a/packages/hoppscotch-data/src/graphql/index.ts b/packages/hoppscotch-data/src/graphql/index.ts index 4a26f5a8..e27b191b 100644 --- a/packages/hoppscotch-data/src/graphql/index.ts +++ b/packages/hoppscotch-data/src/graphql/index.ts @@ -8,6 +8,7 @@ import V5_VERSION from "./v/5" import V6_VERSION from "./v/6" import V7_VERSION from "./v/7" import V8_VERSION from "./v/8" +import V9_VERSION from "./v/9" export { HoppGQLAuthBasic, @@ -19,16 +20,16 @@ export { export { HoppGQLAuthAPIKey } from "./v/4" export { GQLHeader, HoppGQLAuthAWSSignature } from "./v/6" -export { HoppGQLAuth, HoppGQLAuthOAuth2 } from "./v/8" +export { HoppGQLAuth, HoppGQLAuthOAuth2 } from "./v/9" -export const GQL_REQ_SCHEMA_VERSION = 8 +export const GQL_REQ_SCHEMA_VERSION = 9 const versionedObject = z.object({ v: z.number(), }) export const HoppGQLRequest = createVersionedEntity({ - latestVersion: 8, + latestVersion: 9, versionMap: { 1: V1_VERSION, 2: V2_VERSION, @@ -38,6 +39,7 @@ export const HoppGQLRequest = createVersionedEntity({ 6: V6_VERSION, 7: V7_VERSION, 8: V8_VERSION, + 9: V9_VERSION, }, getVersion(x) { const result = versionedObject.safeParse(x) diff --git a/packages/hoppscotch-data/src/graphql/v/9.ts b/packages/hoppscotch-data/src/graphql/v/9.ts new file mode 100644 index 00000000..136d3647 --- /dev/null +++ b/packages/hoppscotch-data/src/graphql/v/9.ts @@ -0,0 +1,94 @@ +import { defineVersion } from "verzod" +import { z } from "zod" + +import { + HoppGQLAuthBasic, + HoppGQLAuthBearer, + HoppGQLAuthInherit, + HoppGQLAuthNone, +} from "./2" +import { HoppGQLAuthAPIKey } from "./4" +import { HoppGQLAuthAWSSignature } from "./6" +import { HoppRESTAuthOAuth2 } from "../../rest/v/15/auth" +import { V8_SCHEMA } from "./8" + +export { HoppRESTAuthOAuth2 as HoppGQLAuthOAuth2 } from "../../rest/v/15/auth" + +export const HoppGQLAuth = z + .discriminatedUnion("authType", [ + HoppGQLAuthNone, + HoppGQLAuthInherit, + HoppGQLAuthBasic, + HoppGQLAuthBearer, + HoppRESTAuthOAuth2, + HoppGQLAuthAPIKey, + HoppGQLAuthAWSSignature, + ]) + .and( + z.object({ + authActive: z.boolean(), + }) + ) + +export type HoppGQLAuth = z.infer + +export const V9_SCHEMA = V8_SCHEMA.extend({ + v: z.literal(9), + auth: HoppGQLAuth, +}) + +export default defineVersion({ + schema: V9_SCHEMA, + initial: false, + up(old: z.infer) { + // If the auth is OAuth2, migrate it to include the new advanced parameters + let newAuth: z.infer + if (old.auth.authType === "oauth-2") { + const oldGrantTypeInfo = old.auth.grantTypeInfo + let newGrantTypeInfo + + // Add the advanced parameters to the appropriate grant type + if (oldGrantTypeInfo.grantType === "AUTHORIZATION_CODE") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + authRequestParams: [], + tokenRequestParams: [], + refreshRequestParams: [], + } + } else if (oldGrantTypeInfo.grantType === "CLIENT_CREDENTIALS") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + tokenRequestParams: [], + refreshRequestParams: [], + } + } else if (oldGrantTypeInfo.grantType === "PASSWORD") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + tokenRequestParams: [], + refreshRequestParams: [], + } + } else if (oldGrantTypeInfo.grantType === "IMPLICIT") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + authRequestParams: [], + refreshRequestParams: [], + } + } else { + newGrantTypeInfo = oldGrantTypeInfo + } + + newAuth = { + ...old.auth, + grantTypeInfo: newGrantTypeInfo, + } as z.infer + } else { + newAuth = old.auth + } + + return { + ...old, + v: 9 as const, + auth: newAuth, + } + }, +}) diff --git a/packages/hoppscotch-data/src/rest-request-response/original-request/index.ts b/packages/hoppscotch-data/src/rest-request-response/original-request/index.ts index 53f4ef09..45a280ef 100644 --- a/packages/hoppscotch-data/src/rest-request-response/original-request/index.ts +++ b/packages/hoppscotch-data/src/rest-request-response/original-request/index.ts @@ -5,6 +5,7 @@ import V2_VERSION from "./v/2" import V3_VERSION from "./v/3" import V4_VERSION from "./v/4" import V5_VERSION from "./v/5" +import V6_VERSION from "./v/6" const versionedObject = z.object({ // v is a stringified number @@ -12,13 +13,14 @@ const versionedObject = z.object({ }) export const HoppRESTResponseOriginalRequest = createVersionedEntity({ - latestVersion: 5, + latestVersion: 6, versionMap: { 1: V1_VERSION, 2: V2_VERSION, 3: V3_VERSION, 4: V4_VERSION, 5: V5_VERSION, + 6: V6_VERSION, }, getVersion(data) { const versionCheck = versionedObject.safeParse(data) @@ -32,7 +34,7 @@ export const HoppRESTResponseOriginalRequest = createVersionedEntity({ }, }) -export const HoppRESTResOriginalReqSchemaVersion = "5" +export const HoppRESTResOriginalReqSchemaVersion = "6" export type HoppRESTResponseOriginalRequest = InferredEntity< typeof HoppRESTResponseOriginalRequest diff --git a/packages/hoppscotch-data/src/rest-request-response/original-request/v/6.ts b/packages/hoppscotch-data/src/rest-request-response/original-request/v/6.ts new file mode 100644 index 00000000..8eb0258d --- /dev/null +++ b/packages/hoppscotch-data/src/rest-request-response/original-request/v/6.ts @@ -0,0 +1,65 @@ +import { defineVersion } from "verzod" +import { z } from "zod" +import { V5_SCHEMA } from "./5" +import { HoppRESTAuth } from "../../../rest/v/15/auth" + +export const V6_SCHEMA = V5_SCHEMA.extend({ + v: z.literal("6"), + auth: HoppRESTAuth, +}) + +export default defineVersion({ + initial: false, + schema: V6_SCHEMA, + up(old: z.infer) { + // If the auth is OAuth2, migrate it to include the new advanced parameters + let newAuth: z.infer + if (old.auth.authType === "oauth-2") { + const oldGrantTypeInfo = old.auth.grantTypeInfo + let newGrantTypeInfo + + // Add the advanced parameters to the appropriate grant type + if (oldGrantTypeInfo.grantType === "AUTHORIZATION_CODE") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + authRequestParams: [], + tokenRequestParams: [], + refreshRequestParams: [], + } + } else if (oldGrantTypeInfo.grantType === "CLIENT_CREDENTIALS") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + tokenRequestParams: [], + refreshRequestParams: [], + } + } else if (oldGrantTypeInfo.grantType === "PASSWORD") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + tokenRequestParams: [], + refreshRequestParams: [], + } + } else if (oldGrantTypeInfo.grantType === "IMPLICIT") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + authRequestParams: [], + refreshRequestParams: [], + } + } else { + newGrantTypeInfo = oldGrantTypeInfo + } + + newAuth = { + ...old.auth, + grantTypeInfo: newGrantTypeInfo, + } as z.infer + } else { + newAuth = old.auth + } + + return { + ...old, + v: "6" as const, + auth: newAuth, + } + }, +}) diff --git a/packages/hoppscotch-data/src/rest/index.ts b/packages/hoppscotch-data/src/rest/index.ts index e03967c2..e429378f 100644 --- a/packages/hoppscotch-data/src/rest/index.ts +++ b/packages/hoppscotch-data/src/rest/index.ts @@ -21,8 +21,9 @@ import { HoppRESTReqBody } from "./v/10/body" import V11_VERSION from "./v/11" import V12_VERSION from "./v/12" import V13_VERSION from "./v/13" -import { HoppRESTAuth } from "./v/13/auth" +import { HoppRESTAuth } from "./v/15/auth" import V14_VERSION from "./v/14" +import V15_VERSION from "./v/15/index" import { HoppRESTRequestResponses } from "../rest-request-response" export * from "./content-types" @@ -36,32 +37,30 @@ export { export { HoppRESTRequestVariables } from "./v/2" -export { ImplicitOauthFlowParams } from "./v/3" - export { HoppRESTAuthAPIKey } from "./v/4" -export { AuthCodeGrantTypeParams } from "./v/5" - export { HoppRESTAuthAWSSignature, HoppRESTHeaders, HoppRESTParams, } from "./v/7" -export { HoppRESTAuthDigest, PasswordGrantTypeParams } from "./v/8/auth" +export { HoppRESTAuthDigest } from "./v/8/auth" export { FormDataKeyValue } from "./v/9/body" -export { - HoppRESTAuthOAuth2, - ClientCredentialsGrantTypeParams, -} from "./v/11/auth" - export { HoppRESTReqBody } from "./v/10/body" export { HoppRESTAuthHAWK, HoppRESTAuthAkamaiEdgeGrid } from "./v/12/auth" -export { HoppRESTAuth, HoppRESTAuthJWT } from "./v/13/auth" +export { HoppRESTAuth, HoppRESTAuthJWT } from "./v/15/auth" +export { AuthCodeGrantTypeParams } from "./v/15/auth" +export { PasswordGrantTypeParams } from "./v/15/auth" +export { ImplicitOauthFlowParams } from "./v/15/auth" +export { + HoppRESTAuthOAuth2, + ClientCredentialsGrantTypeParams, +} from "./v/15/auth" export { HoppRESTRequestResponse, @@ -74,7 +73,7 @@ const versionedObject = z.object({ }) export const HoppRESTRequest = createVersionedEntity({ - latestVersion: 14, + latestVersion: 15, versionMap: { 0: V0_VERSION, 1: V1_VERSION, @@ -91,6 +90,7 @@ export const HoppRESTRequest = createVersionedEntity({ 12: V12_VERSION, 13: V13_VERSION, 14: V14_VERSION, + 15: V15_VERSION, }, getVersion(data) { // For V1 onwards we have the v string storing the number @@ -133,7 +133,7 @@ const HoppRESTRequestEq = Eq.struct({ responses: lodashIsEqualEq, }) -export const RESTReqSchemaVersion = "14" +export const RESTReqSchemaVersion = "15" export type HoppRESTParam = HoppRESTRequest["params"][number] export type HoppRESTHeader = HoppRESTRequest["headers"][number] diff --git a/packages/hoppscotch-data/src/rest/v/15/auth.ts b/packages/hoppscotch-data/src/rest/v/15/auth.ts new file mode 100644 index 00000000..eace3b74 --- /dev/null +++ b/packages/hoppscotch-data/src/rest/v/15/auth.ts @@ -0,0 +1,90 @@ +import { z } from "zod" +import { + HoppRESTAuthBasic, + HoppRESTAuthBearer, + HoppRESTAuthInherit, + HoppRESTAuthNone, +} from "../1" +import { HoppRESTAuthAPIKey } from "../4" +import { AuthCodeGrantTypeParams as AuthCodeGrantTypeParamsOld } from "../7" +import { HoppRESTAuthAWSSignature } from "../7" +import { + HoppRESTAuthDigest, + PasswordGrantTypeParams as PasswordGrantTypeParamsOld, +} from "../8/auth" +import { HoppRESTAuthAkamaiEdgeGrid, HoppRESTAuthHAWK } from "../12/auth" +import { HoppRESTAuthJWT } from "../13/auth" +import { ClientCredentialsGrantTypeParams as ClientCredentialsGrantTypeParamsOld } from "../11/auth" +import { ImplicitOauthFlowParams as ImplicitOauthFlowParamsOld } from "../3" + +export { HoppRESTAuthJWT } from "../13/auth" + +// Define the OAuth2 advanced parameter structure +const OAuth2AdvancedParam = z.object({ + id: z.number(), + key: z.string(), + value: z.string(), + active: z.boolean(), + sendIn: z.enum(["headers", "url", "body"]).catch("headers"), +}) + +// omit sendIn from OAuth2AuthRequestParam +const OAuth2AuthRequestParam = OAuth2AdvancedParam.omit({ sendIn: true }) + +export const AuthCodeGrantTypeParams = AuthCodeGrantTypeParamsOld.extend({ + authRequestParams: z.array(OAuth2AuthRequestParam).optional().default([]), + tokenRequestParams: z.array(OAuth2AdvancedParam).optional().default([]), + refreshRequestParams: z.array(OAuth2AdvancedParam).optional().default([]), +}) + +export const ClientCredentialsGrantTypeParams = + ClientCredentialsGrantTypeParamsOld.extend({ + tokenRequestParams: z.array(OAuth2AdvancedParam).optional().default([]), + refreshRequestParams: z.array(OAuth2AdvancedParam).optional().default([]), + }) + +export const PasswordGrantTypeParams = PasswordGrantTypeParamsOld.extend({ + tokenRequestParams: z.array(OAuth2AdvancedParam).optional().default([]), + refreshRequestParams: z.array(OAuth2AdvancedParam).optional().default([]), +}) + +export const ImplicitOauthFlowParams = ImplicitOauthFlowParamsOld.extend({ + authRequestParams: z.array(OAuth2AuthRequestParam).optional().default([]), + refreshRequestParams: z.array(OAuth2AdvancedParam).optional().default([]), +}) + +// Extend OAuth2 with advanced parameters +export const HoppRESTAuthOAuth2 = z.object({ + authType: z.literal("oauth-2"), + grantTypeInfo: z.discriminatedUnion("grantType", [ + AuthCodeGrantTypeParams, + ClientCredentialsGrantTypeParams, + PasswordGrantTypeParams, + ImplicitOauthFlowParams, + ]), + addTo: z.enum(["HEADERS", "QUERY_PARAMS"]).catch("HEADERS"), +}) + +export type HoppRESTAuthOAuth2 = z.infer + +export const HoppRESTAuth = z + .discriminatedUnion("authType", [ + HoppRESTAuthNone, + HoppRESTAuthInherit, + HoppRESTAuthBasic, + HoppRESTAuthBearer, + HoppRESTAuthOAuth2, + HoppRESTAuthAPIKey, + HoppRESTAuthAWSSignature, + HoppRESTAuthDigest, + HoppRESTAuthHAWK, + HoppRESTAuthAkamaiEdgeGrid, + HoppRESTAuthJWT, + ]) + .and( + z.object({ + authActive: z.boolean(), + }) + ) + +export type HoppRESTAuth = z.infer diff --git a/packages/hoppscotch-data/src/rest/v/15/index.ts b/packages/hoppscotch-data/src/rest/v/15/index.ts new file mode 100644 index 00000000..d7ddced6 --- /dev/null +++ b/packages/hoppscotch-data/src/rest/v/15/index.ts @@ -0,0 +1,69 @@ +import { V14_SCHEMA } from "../14" + +import { z } from "zod" +import { defineVersion } from "verzod" + +import { HoppRESTAuth } from "./auth" + +export const V15_SCHEMA = V14_SCHEMA.extend({ + v: z.literal("15"), + auth: HoppRESTAuth, +}) + +const V15_VERSION = defineVersion({ + schema: V15_SCHEMA, + initial: false, + up(old: z.infer) { + // If the auth is OAuth2, migrate it to include the new advanced parameters + let newAuth: z.infer + if (old.auth.authType === "oauth-2") { + const oldGrantTypeInfo = old.auth.grantTypeInfo + let newGrantTypeInfo + + // Add the advanced parameters to the appropriate grant type + if (oldGrantTypeInfo.grantType === "AUTHORIZATION_CODE") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + authRequestParams: [], + tokenRequestParams: [], + refreshRequestParams: [], + } + } else if (oldGrantTypeInfo.grantType === "CLIENT_CREDENTIALS") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + tokenRequestParams: [], + refreshRequestParams: [], + } + } else if (oldGrantTypeInfo.grantType === "PASSWORD") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + tokenRequestParams: [], + refreshRequestParams: [], + } + } else if (oldGrantTypeInfo.grantType === "IMPLICIT") { + newGrantTypeInfo = { + ...oldGrantTypeInfo, + authRequestParams: [], + refreshRequestParams: [], + } + } else { + newGrantTypeInfo = oldGrantTypeInfo + } + + newAuth = { + ...old.auth, + grantTypeInfo: newGrantTypeInfo, + } as z.infer + } else { + newAuth = old.auth + } + + return { + ...old, + v: "15" as const, + auth: newAuth, + } + }, +}) + +export default V15_VERSION diff --git a/packages/hoppscotch-selfhost-desktop/src/platform/collections/collections.platform.ts b/packages/hoppscotch-selfhost-desktop/src/platform/collections/collections.platform.ts index 42aa9f7a..8f1c306f 100644 --- a/packages/hoppscotch-selfhost-desktop/src/platform/collections/collections.platform.ts +++ b/packages/hoppscotch-selfhost-desktop/src/platform/collections/collections.platform.ts @@ -134,7 +134,7 @@ function exportedCollectionToHoppCollection( return { id: restCollection.id, - v: 8, + v: 9, name: restCollection.name, folders: restCollection.folders.map((folder) => exportedCollectionToHoppCollection(folder, collectionType) @@ -196,7 +196,7 @@ function exportedCollectionToHoppCollection( return { id: gqlCollection.id, - v: 8, + v: 9, name: gqlCollection.name, folders: gqlCollection.folders.map((folder) => exportedCollectionToHoppCollection(folder, collectionType) @@ -374,7 +374,7 @@ function setupUserCollectionCreatedSubscription() { name: res.right.userCollectionCreated.title, folders: [], requests: [], - v: 8, + v: 9, auth: data.auth, headers: addDescriptionField(data.headers), }) @@ -382,7 +382,7 @@ function setupUserCollectionCreatedSubscription() { name: res.right.userCollectionCreated.title, folders: [], requests: [], - v: 8, + v: 9, auth: data.auth, headers: addDescriptionField(data.headers), }) @@ -607,7 +607,7 @@ function setupUserCollectionDuplicatedSubscription() { name, folders, requests, - v: 8, + v: 9, auth, headers: addDescriptionField(headers), } @@ -1037,7 +1037,7 @@ function transformDuplicatedCollections( name, folders, requests, - v: 8, + v: 9, auth, headers: addDescriptionField(headers), } diff --git a/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/index.ts b/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/index.ts index a9d68e57..08899524 100644 --- a/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/index.ts +++ b/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/index.ts @@ -140,7 +140,7 @@ function exportedCollectionToHoppCollection( return { id: restCollection.id, _ref_id: data._ref_id ?? generateUniqueRefId("coll"), - v: 8, + v: 9, name: restCollection.name, folders: restCollection.folders.map((folder) => exportedCollectionToHoppCollection(folder, collectionType) @@ -204,7 +204,7 @@ function exportedCollectionToHoppCollection( return { id: gqlCollection.id, _ref_id: data._ref_id ?? generateUniqueRefId("coll"), - v: 8, + v: 9, name: gqlCollection.name, folders: gqlCollection.folders.map((folder) => exportedCollectionToHoppCollection(folder, collectionType) @@ -383,7 +383,7 @@ function setupUserCollectionCreatedSubscription() { name: res.right.userCollectionCreated.title, folders: [], requests: [], - v: 8, + v: 9, _ref_id: data._ref_id, auth: data.auth, headers: addDescriptionField(data.headers), @@ -392,7 +392,7 @@ function setupUserCollectionCreatedSubscription() { name: res.right.userCollectionCreated.title, folders: [], requests: [], - v: 8, + v: 9, _ref_id: data._ref_id, auth: data.auth, headers: addDescriptionField(data.headers), @@ -620,7 +620,7 @@ function setupUserCollectionDuplicatedSubscription() { name, folders, requests, - v: 8, + v: 9, _ref_id, auth, headers: addDescriptionField(headers), @@ -1054,7 +1054,7 @@ function transformDuplicatedCollections( folders, requests, _ref_id, - v: 8, + v: 9, auth, headers: addDescriptionField(headers), } diff --git a/packages/hoppscotch-selfhost-web/src/platform/collections/web/index.ts b/packages/hoppscotch-selfhost-web/src/platform/collections/web/index.ts index 1f3a7de5..73e34905 100644 --- a/packages/hoppscotch-selfhost-web/src/platform/collections/web/index.ts +++ b/packages/hoppscotch-selfhost-web/src/platform/collections/web/index.ts @@ -140,7 +140,7 @@ function exportedCollectionToHoppCollection( return { id: restCollection.id, _ref_id: data._ref_id ?? generateUniqueRefId("coll"), - v: 8, + v: 9, name: restCollection.name, folders: restCollection.folders.map((folder) => exportedCollectionToHoppCollection(folder, collectionType) @@ -204,7 +204,7 @@ function exportedCollectionToHoppCollection( return { id: gqlCollection.id, _ref_id: data._ref_id ?? generateUniqueRefId("coll"), - v: 8, + v: 9, name: gqlCollection.name, folders: gqlCollection.folders.map((folder) => exportedCollectionToHoppCollection(folder, collectionType) @@ -383,7 +383,7 @@ function setupUserCollectionCreatedSubscription() { name: res.right.userCollectionCreated.title, folders: [], requests: [], - v: 8, + v: 9, _ref_id: data._ref_id, auth: data.auth, headers: addDescriptionField(data.headers), @@ -392,7 +392,7 @@ function setupUserCollectionCreatedSubscription() { name: res.right.userCollectionCreated.title, folders: [], requests: [], - v: 8, + v: 9, _ref_id: data._ref_id, auth: data.auth, headers: addDescriptionField(data.headers), @@ -620,7 +620,7 @@ function setupUserCollectionDuplicatedSubscription() { name, folders, requests, - v: 8, + v: 9, _ref_id, auth, headers: addDescriptionField(headers), @@ -1054,7 +1054,7 @@ function transformDuplicatedCollections( folders, requests, _ref_id, - v: 8, + v: 9, auth, headers: addDescriptionField(headers), }