Compare commits

...

14 Commits

Author SHA1 Message Date
nivedin
b5e7577024 fix: saving teamRequest not inheriting properties 2023-12-23 16:12:55 +05:30
nivedin
9bd7961c15 chore: minor padding update 2023-12-22 12:29:06 +05:30
nivedin
4f74cd8f89 chore: minor code update 2023-12-22 12:07:17 +05:30
nivedin
cf4ecd326e chore: minor update for team header-auth properties 2023-12-20 22:22:05 +05:30
nivedin
2ed43f03e1 fix: add loading and error state 2023-12-20 22:19:17 +05:30
nivedin
1ac518ef87 fix: update i18n text 2023-12-20 22:18:27 +05:30
Nivedin
7db7b9b068 fix: fallback section for embeds if invalid link (#3673) 2023-12-19 18:37:44 +05:30
James George
3d25ef48d1 fix(persistence-service): update schemas found to differ in runtime (#3671) 2023-12-19 18:34:27 +05:30
Mir Arif Hasan
4f138beb8a chore: db migration missing message (#3672) 2023-12-19 18:42:00 +06:00
James George
3d7a76bced chore(common): Gist export flow updates (#3665) 2023-12-19 17:37:35 +05:30
Nivedin
74359ea74e fix: auth-header not inheriting properties (#3668) 2023-12-19 17:04:24 +05:30
Mir Arif Hasan
a694d3f7eb hotfix: added validation on infra config update (#3667)
* feat: added validation on infra config update

* chore: removed async keyword

* fix: feedback
2023-12-19 17:15:46 +06:00
Nivedin
58a9514b67 fix: gql history schema error (#3662) 2023-12-19 16:39:32 +05:30
Akash K
a75bfa9d9e fix: actions not working when sidebar is hidden (#3669) 2023-12-19 16:13:59 +05:30
23 changed files with 326 additions and 125 deletions

View File

@@ -676,9 +676,16 @@ export const INFRA_CONFIG_RESET_FAILED = 'infra_config/reset_failed' as const;
*/
export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
/**
* Infra Config service (auth provider/mailer/audit logs) not configured
* (InfraConfigService)
*/
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
'infra_config/service_not_configured' as const;
/**
* Error message for when the database table does not exist
* (InfraConfigService)
*/
export const DATABASE_TABLE_NOT_EXIST =
'Database migration not performed. Please check the FAQ for assistance: https://docs.hoppscotch.io/support/getting-started/faq';
'Database migration not found. Please check the documentation for assistance: https://docs.hoppscotch.io/documentation/self-host/community-edition/install-and-build#running-migrations';

View File

@@ -15,11 +15,13 @@ import {
INFRA_CONFIG_NOT_LISTED,
INFRA_CONFIG_RESET_FAILED,
INFRA_CONFIG_UPDATE_FAILED,
INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
} from 'src/errors';
import { throwErr, validateEmail, validateSMTPUrl } from 'src/utils';
import { ConfigService } from '@nestjs/config';
import { ServiceStatus, stopApp } from './helper';
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
import { AuthProvider } from 'src/auth/helper';
@Injectable()
export class InfraConfigService implements OnModuleInit {
@@ -124,7 +126,7 @@ export class InfraConfigService implements OnModuleInit {
cast(dbInfraConfig: DBInfraConfig) {
return <InfraConfig>{
name: dbInfraConfig.name,
value: dbInfraConfig.value,
value: dbInfraConfig.value ?? '',
};
}
@@ -182,6 +184,38 @@ export class InfraConfigService implements OnModuleInit {
}
}
/**
* Check if the service is configured or not
* @param service Service can be Auth Provider, Mailer, Audit Log etc.
* @returns Either true or false
*/
isServiceConfigured(service: AuthProvider) {
switch (service) {
case AuthProvider.GOOGLE:
return (
this.configService.get<string>('INFRA.GOOGLE_CLIENT_ID') &&
this.configService.get<string>('INFRA.GOOGLE_CLIENT_SECRET')
);
case AuthProvider.GITHUB:
return (
this.configService.get<string>('INFRA.GITHUB_CLIENT_ID') &&
!this.configService.get<string>('INFRA.GITHUB_CLIENT_SECRET')
);
case AuthProvider.MICROSOFT:
return (
this.configService.get<string>('INFRA.MICROSOFT_CLIENT_ID') &&
!this.configService.get<string>('INFRA.MICROSOFT_CLIENT_SECRET')
);
case AuthProvider.EMAIL:
return (
this.configService.get<string>('INFRA.MAILER_SMTP_URL') &&
this.configService.get<string>('INFRA.MAILER_ADDRESS_FROM')
);
default:
return false;
}
}
/**
* Enable or Disable SSO for login/signup
* @param provider Auth Provider to enable or disable
@@ -195,15 +229,21 @@ export class InfraConfigService implements OnModuleInit {
let updatedAuthProviders = allowedAuthProviders;
providerInfo.forEach(({ provider, status }) => {
for (let i = 0; i < providerInfo.length; i++) {
const { provider, status } = providerInfo[i];
if (status === ServiceStatus.ENABLE) {
const isConfigured = this.isServiceConfigured(provider);
if (!isConfigured) {
throwErr(INFRA_CONFIG_SERVICE_NOT_CONFIGURED);
}
updatedAuthProviders.push(provider);
} else if (status === ServiceStatus.DISABLE) {
updatedAuthProviders = updatedAuthProviders.filter(
(p) => p !== provider,
);
}
});
}
updatedAuthProviders = [...new Set(updatedAuthProviders)];
@@ -286,6 +326,9 @@ export class InfraConfigService implements OnModuleInit {
}
}
/**
* Validate the values of the InfraConfigs
*/
validateEnvValues(
infraConfigs: {
name: InfraConfigEnumForClient | InfraConfigEnum;
@@ -302,6 +345,24 @@ export class InfraConfigService implements OnModuleInit {
const isValidEmail = validateEmail(infraConfigs[i].value);
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.GOOGLE_CLIENT_ID:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.GOOGLE_CLIENT_SECRET:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.GITHUB_CLIENT_ID:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.GITHUB_CLIENT_SECRET:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.MICROSOFT_CLIENT_ID:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.MICROSOFT_CLIENT_SECRET:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
default:
break;
}

View File

@@ -121,7 +121,7 @@
"generate_token": "Generate Token",
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
"include_in_url": "Include in URL",
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
"inherited_from": "Inherited {auth} from parent collection {collection} ",
"learn": "Learn how",
"oauth": {
"redirect_auth_server_returned_error": "Auth Server returned an error state",
@@ -295,6 +295,7 @@
"incorrect_email": "Incorrect email",
"invalid_link": "Invalid link",
"invalid_link_description": "The link you clicked is invalid or expired.",
"invalid_embed_link": "The embed does not exist or is invalid.",
"json_parsing_failed": "Invalid JSON",
"json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again",
"network_error": "There seems to be a network error. Please try again.",
@@ -313,10 +314,12 @@
"export": {
"as_json": "Export as JSON",
"create_secret_gist": "Create secret Gist",
"create_secret_gist_tooltip_text": "Export as secret Gist",
"failed": "Something went wrong while exporting",
"gist_created": "Gist created",
"secret_gist_success": "Successfully exported as secret Gist",
"require_github": "Login with GitHub to create secret gist",
"title": "Export"
"title": "Export",
"success": "Successfully exported"
},
"filter": {
"all": "All",

View File

@@ -1,15 +1,16 @@
<template>
<Splitpanes
class="smart-splitter"
:rtl="SIDEBAR_ON_LEFT && mdAndLarger"
:class="{
'!flex-row-reverse': SIDEBAR_ON_LEFT && mdAndLarger,
'smart-splitter': SIDEBAR && hasSidebar,
'no-splitter': !(SIDEBAR && hasSidebar),
}"
:horizontal="!mdAndLarger"
@resize="setPaneEvent($event, 'vertical')"
>
<Pane
:size="PANE_MAIN_SIZE"
:size="SIDEBAR && hasSidebar ? PANE_MAIN_SIZE : 100"
min-size="65"
class="flex flex-col !overflow-auto"
>
@@ -36,9 +37,8 @@
</Splitpanes>
</Pane>
<Pane
v-if="SIDEBAR && hasSidebar"
:size="PANE_SIDEBAR_SIZE"
min-size="25"
:size="SIDEBAR && hasSidebar ? PANE_SIDEBAR_SIZE : 0"
:min-size="25"
class="flex flex-col !overflow-auto bg-primaryContrast"
>
<slot name="sidebar" />

View File

@@ -9,7 +9,10 @@
</template>
<script setup lang="ts">
import { HoppCollection } from "@hoppscotch/data"
import * as E from "fp-ts/Either"
import { PropType, computed, ref } from "vue"
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
import { UrlSource } from "~/helpers/import-export/import/import-sources/UrlSource"
@@ -24,11 +27,9 @@ import {
} from "~/helpers/import-export/import/importers"
import { defineStep } from "~/composables/step-components"
import { PropType, computed, ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { HoppCollection } from "@hoppscotch/data"
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
@@ -48,7 +49,7 @@ import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
import { platform } from "~/platform"
import { initializeDownloadCollection } from "~/helpers/import-export/export"
import { collectionsGistExporter } from "~/helpers/import-export/export/gistExport"
import { gistExporter } from "~/helpers/import-export/export/gist"
import { myCollectionsExporter } from "~/helpers/import-export/export/myCollections"
import { teamCollectionsExporter } from "~/helpers/import-export/export/teamCollections"
@@ -83,6 +84,8 @@ const currentUser = useReadonlyStream(
platform.auth.getCurrentUser()
)
const myCollections = useReadonlyStream(restCollections$, [])
const showImportFailedError = () => {
toast.error(t("import.failed"))
}
@@ -468,8 +471,13 @@ const HoppGistCollectionsExporter: ImporterOrExporter = {
icon: IconGithub,
disabled: !currentUser.value
? true
: currentUser.value.provider !== "github.com",
title: t("export.create_secret_gist"),
: currentUser.value?.provider !== "github.com",
title:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
currentUser?.value?.provider === "github.com"
? "export.create_secret_gist_tooltip_text"
: "export.require_github",
applicableTo: ["personal-workspace", "team-workspace"],
isLoading: isHoppGistCollectionExporterInProgress,
},
@@ -486,13 +494,27 @@ const HoppGistCollectionsExporter: ImporterOrExporter = {
}
if (E.isRight(collectionJSON)) {
collectionsGistExporter(collectionJSON.right, accessToken)
if (!JSON.parse(collectionJSON.right).length) {
isHoppGistCollectionExporterInProgress.value = false
return toast.error(t("error.no_collections_to_export"))
}
const res = await gistExporter(collectionJSON.right, accessToken)
if (E.isLeft(res)) {
toast.error(t("export.failed"))
return
}
toast.success(t("export.secret_gist_success"))
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "gist",
platform: "rest",
})
platform.io.openExternalLink(res.right)
}
isHoppGistCollectionExporterInProgress.value = false
@@ -560,8 +582,6 @@ const selectedTeamID = computed(() => {
: undefined
})
const myCollections = useReadonlyStream(restCollections$, [])
const getCollectionJSON = async () => {
if (
props.collectionsType.type === "team-collections" &&

View File

@@ -33,6 +33,7 @@
@select="onSelect"
@update-team="updateTeam"
@update-collection-type="updateCollectionType"
@set-team-collection-adapter="teamCollectionAdapter = $event"
/>
</div>
</template>
@@ -86,6 +87,7 @@ import { platform } from "~/platform"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { GQLTabService } from "~/services/tab/graphql"
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
const t = useI18n()
const toast = useToast()
@@ -93,6 +95,8 @@ const toast = useToast()
const RESTTabs = useService(RESTTabService)
const GQLTabs = useService(GQLTabService)
const teamCollectionAdapter = ref<TeamCollectionAdapter>()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType =
@@ -380,7 +384,6 @@ const saveRequestAs = async () => {
platform: "rest",
workspaceType: "team",
})
pipe(
updateTeamRequest(picked.value.requestID, data),
TE.match(
@@ -388,7 +391,31 @@ const saveRequestAs = async () => {
toast.error(`${getErrorMessage(err)}`)
modalLoadingState.value = false
},
() => {
(result) => {
const { updateRequest } = result
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
saveContext: {
originLocation: "team-collection",
requestID: updateRequest.id,
collectionID: updateRequest.collectionID,
},
}
if (teamCollectionAdapter.value) {
const { auth, headers } =
teamCollectionAdapter.value.cascadeParentCollectionForHeaderAuth(
updateRequest.collectionID
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
}
modalLoadingState.value = false
requestSaved()
}
@@ -516,6 +543,18 @@ const updateTeamCollectionOrFolder = (
},
}
if (teamCollectionAdapter.value) {
const { auth, headers } =
teamCollectionAdapter.value.cascadeParentCollectionForHeaderAuth(
createRequestInCollection.collection.id
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
}
modalLoadingState.value = false
requestSaved()
}

View File

@@ -9,15 +9,16 @@
</template>
<script setup lang="ts">
import { HoppCollection } from "@hoppscotch/data"
import * as E from "fp-ts/Either"
import { ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { HoppCollection } from "@hoppscotch/data"
import { ImporterOrExporter } from "~/components/importExport/types"
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
import * as E from "fp-ts/Either"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconUser from "~icons/lucide/user"
import { initializeDownloadCollection } from "~/helpers/import-export/export"
@@ -30,7 +31,7 @@ import {
} from "~/newstore/collections"
import { hoppGqlCollectionsImporter } from "~/helpers/import-export/import/hoppGql"
import { gqlCollectionsExporter } from "~/helpers/import-export/export/gqlCollections"
import { gqlCollectionsGistExporter } from "~/helpers/import-export/export/gqlCollectionsGistExporter"
import { gistExporter } from "~/helpers/import-export/export/gist"
import { computed } from "vue"
import { hoppGQLImporter } from "~/helpers/import-export/import/hopp"
@@ -42,6 +43,10 @@ const currentUser = useReadonlyStream(
platform.auth.getCurrentUser()
)
const gqlCollections = useReadonlyStream(graphqlCollections$, [])
const isGqlCollectionGistExportInProgress = ref(false)
const GqlCollectionsHoppImporter: ImporterOrExporter = {
metadata: {
id: "import.from_json",
@@ -119,8 +124,6 @@ const GqlCollectionsGistImporter: ImporterOrExporter = {
}),
}
const gqlCollections = useReadonlyStream(graphqlCollections$, [])
const GqlCollectionsHoppExporter: ImporterOrExporter = {
metadata: {
id: "export.as_json",
@@ -159,29 +162,35 @@ const GqlCollectionsGistExporter: ImporterOrExporter = {
metadata: {
id: "export.as_gist",
name: "export.create_secret_gist",
title: !currentUser
? "export.require_github"
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
currentUser.provider !== "github.com"
? `export.require_github`
: "export.create_secret_gist",
title:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
currentUser?.value?.provider === "github.com"
? "export.create_secret_gist_tooltip_text"
: "export.require_github",
icon: IconUser,
disabled: !currentUser.value
? true
: currentUser.value.provider !== "github.com",
: currentUser.value?.provider !== "github.com",
applicableTo: ["personal-workspace"],
isLoading: isGqlCollectionGistExportInProgress,
},
action: async () => {
if (!gqlCollections.value.length) {
return toast.error(t("error.no_collections_to_export"))
}
if (!currentUser.value) {
toast.error(t("profile.no_permission"))
return
}
isGqlCollectionGistExportInProgress.value = true
const accessToken = currentUser.value?.accessToken
if (accessToken) {
const res = await gqlCollectionsGistExporter(
const res = await gistExporter(
JSON.stringify(gqlCollections.value),
accessToken
)
@@ -191,7 +200,7 @@ const GqlCollectionsGistExporter: ImporterOrExporter = {
return
}
toast.success(t("export.success"))
toast.success(t("export.secret_gist_success"))
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
@@ -201,6 +210,8 @@ const GqlCollectionsGistExporter: ImporterOrExporter = {
platform.io.openExternalLink(res.right)
}
isGqlCollectionGistExportInProgress.value = false
},
}

View File

@@ -262,6 +262,7 @@ const emit = defineEmits<{
(event: "select", payload: Picked | null): void
(event: "update-team", team: SelectedTeam): void
(event: "update-collection-type", type: CollectionType["type"]): void
(event: "set-team-collection-adapter", adapter: TeamCollectionAdapter): void
}>()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
@@ -354,6 +355,7 @@ watch(
(newTeam) => {
if (newTeam) {
teamCollectionAdapter.changeTeamID(newTeam.id)
emit("set-team-collection-adapter", teamCollectionAdapter)
}
}
)

View File

@@ -9,15 +9,17 @@
</template>
<script setup lang="ts">
import { Environment } from "@hoppscotch/data"
import * as E from "fp-ts/Either"
import { ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { Environment } from "@hoppscotch/data"
import { ImporterOrExporter } from "~/components/importExport/types"
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
import { hoppEnvImporter } from "~/helpers/import-export/import/hoppEnv"
import * as E from "fp-ts/Either"
import {
appendEnvironments,
addGlobalEnvVariable,
@@ -39,7 +41,7 @@ import { initializeDownloadCollection } from "~/helpers/import-export/export"
import { computed } from "vue"
import { useReadonlyStream } from "~/composables/stream"
import { environmentsExporter } from "~/helpers/import-export/export/environments"
import { environmentsGistExporter } from "~/helpers/import-export/export/environmentsGistExport"
import { gistExporter } from "~/helpers/import-export/export/gist"
import { platform } from "~/platform"
const t = useI18n()
@@ -58,6 +60,8 @@ const currentUser = useReadonlyStream(
platform.auth.getCurrentUser()
)
const isEnvironmentGistExportInProgress = ref(false)
const isTeamEnvironment = computed(() => {
return props.environmentType === "TEAM_ENV"
})
@@ -262,35 +266,44 @@ const HoppEnvironmentsGistExporter: ImporterOrExporter = {
title:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
currentUser?.provider === "github.com"
? "export.create_secret_gist"
currentUser?.value?.provider === "github.com"
? "export.create_secret_gist_tooltip_text"
: "export.require_github",
icon: IconUser,
disabled: !currentUser.value
? true
: currentUser.value.provider !== "github.com",
: currentUser.value?.provider !== "github.com",
applicableTo: ["personal-workspace", "team-workspace"],
isLoading: isEnvironmentGistExportInProgress,
},
action: async () => {
if (!environmentJson.value.length) {
return toast.error(t("error.no_environments_to_export"))
}
if (!currentUser.value) {
toast.error(t("profile.no_permission"))
return
}
isEnvironmentGistExportInProgress.value = true
const accessToken = currentUser.value?.accessToken
if (accessToken) {
const res = await environmentsGistExporter(
const res = await gistExporter(
JSON.stringify(environmentJson.value),
accessToken
accessToken,
"hoppscotch-environment.json"
)
if (E.isLeft(res)) {
toast.error(t("export.failed"))
isEnvironmentGistExportInProgress.value = false
return
}
toast.success(t("export.success"))
toast.success(t("export.secret_gist_success"))
platform.analytics?.logEvent({
type: "HOPP_EXPORT_ENVIRONMENT",
@@ -299,6 +312,8 @@ const HoppEnvironmentsGistExporter: ImporterOrExporter = {
platform.io.openExternalLink(res.right)
}
isEnvironmentGistExportInProgress.value = false
},
}

View File

@@ -140,7 +140,10 @@ const runQuery = async (
const runVariables = clone(request.value.variables)
const runAuth =
request.value.auth.authType === "inherit" && request.value.auth.authActive
? clone(tabs.currentActiveTab.value.document.inheritedProperties?.auth)
? clone(
tabs.currentActiveTab.value.document.inheritedProperties?.auth
.inheritedAuth
)
: clone(request.value.auth)
const inheritedHeaders =

View File

@@ -21,7 +21,7 @@
/>
</div>
<div class="flex flex-col">
<div v-if="loading" class="flex flex-col items-center justify-center">
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
<HoppSmartSpinner class="mb-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
@@ -451,7 +451,7 @@ const getErrorMessage = (err: GQLError<string>) => {
}
switch (err.error) {
case "shortcode/not_found":
return t("shared_request.not_found")
return t("shared_requests.not_found")
default:
return t("error.something_went_wrong")
}

View File

@@ -2,5 +2,6 @@ mutation UpdateRequest($data: UpdateTeamRequestInput!, $requestID: ID!) {
updateRequest(data: $data, requestID: $requestID) {
id
title
collectionID
}
}

View File

@@ -27,7 +27,7 @@ export const getDefaultGQLRequest = (): HoppGQLRequest => ({
}`,
query: DEFAULT_QUERY,
auth: {
authType: "inherit",
authType: "none",
authActive: true,
},
})

View File

@@ -1,18 +0,0 @@
import * as E from "fp-ts/Either"
import { createGist } from "~/helpers/gist"
export const environmentsGistExporter = async (
environmentsJSON: string,
accessToken: string
) => {
const res = await createGist(
environmentsJSON,
"hoppscotch-collections.json",
accessToken
)()
if (E.isLeft(res)) {
return E.left(res.left)
}
return E.right(res.right.data.html_url as string)
}

View File

@@ -0,0 +1,19 @@
import * as E from "fp-ts/Either"
import { createGist } from "~/helpers/gist"
export const gistExporter = async (
JSONFileContents: string,
accessToken: string,
fileName = "hoppscotch-collections.json"
) => {
if (!accessToken) {
return E.left("Invalid User")
}
const res = await createGist(JSONFileContents, fileName, accessToken)()
if (E.isLeft(res)) {
return E.left(res.left)
}
return E.right(res.right.data.html_url as string)
}

View File

@@ -1,22 +0,0 @@
import { createGist } from "~/helpers/gist"
import * as E from "fp-ts/Either"
export const collectionsGistExporter = async (
collectionJSON: string,
accessToken: string
) => {
if (!accessToken) {
return E.left("Invalid User")
}
const res = await createGist(
collectionJSON,
"hoppscotch-collections.json",
accessToken
)()
if (E.isLeft(res)) {
return E.left(res.left)
}
return E.right(true)
}

View File

@@ -1,18 +0,0 @@
import * as E from "fp-ts/Either"
import { createGist } from "~/helpers/gist"
export const gqlCollectionsGistExporter = async (
gqlCollectionsJSON: string,
accessToken: string
) => {
const res = await createGist(
gqlCollectionsJSON,
"hoppscotch-collections.json",
accessToken
)()
if (E.isLeft(res)) {
return E.left(res.left)
}
return E.right(res.right.data.html_url as string)
}

View File

@@ -156,6 +156,7 @@ export default class ShortcodeListAdapter {
const [shortcodeCreated$, shortcodeCreatedSub] = runAuthOnlyGQLSubscription(
{
query: ShortcodeCreatedDocument,
variables: {},
}
)
@@ -172,6 +173,7 @@ export default class ShortcodeListAdapter {
const [shortcodeRevoked$, shortcodeRevokedSub] = runAuthOnlyGQLSubscription(
{
query: ShortcodeDeletedDocument,
variables: {},
}
)
@@ -188,6 +190,7 @@ export default class ShortcodeListAdapter {
const [shortcodeUpdated$, shortcodeUpdatedSub] = runAuthOnlyGQLSubscription(
{
query: ShortcodeUpdatedDocument,
variables: {},
}
)

View File

@@ -1034,6 +1034,11 @@ export default class NewTeamCollectionAdapter {
}
}
/**
* Used to obtain the inherited auth and headers for a given folder path, used for both REST and GraphQL team collections
* @param folderPath the path of the folder to cascade the auth from
* @returns the inherited auth and headers for the given folder path
*/
public cascadeParentCollectionForHeaderAuth(folderPath: string) {
let auth: HoppInheritedProperty["auth"] = {
parentID: folderPath ?? "",
@@ -1089,7 +1094,10 @@ export default class NewTeamCollectionAdapter {
const parentFolderAuth = data.auth
const parentFolderHeaders = data.headers
if (parentFolderAuth?.authType === "inherit" && path.length === 1) {
if (
parentFolderAuth?.authType === "inherit" &&
[...path.slice(0, i + 1)].length === 1
) {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
parentName: parentFolder.title,

View File

@@ -4,6 +4,7 @@ import {
HoppRESTRequest,
HoppCollection,
makeCollection,
HoppGQLAuth,
} from "@hoppscotch/data"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { cloneDeep } from "lodash-es"
@@ -11,6 +12,9 @@ import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { HoppRESTAuth } from "@hoppscotch/data"
import { HoppRESTHeaders } from "@hoppscotch/data"
import { HoppGQLHeader } from "~/helpers/graphql"
const defaultRESTCollectionState = {
state: [
@@ -63,6 +67,12 @@ export function navigateToFolderWithIndexPath(
return target !== undefined ? target : null
}
/**
* Used to obtain the inherited auth and headers for a given folder path, used for both REST and GraphQL personal collections
* @param folderPath the path of the folder to cascade the auth from
* @param type the type of collection
* @returns the inherited auth and headers for the given folder path
*/
export function cascadeParentCollectionForHeaderAuth(
folderPath: string | undefined,
type: "rest" | "graphql"
@@ -103,10 +113,16 @@ export function cascadeParentCollectionForHeaderAuth(
return { auth, headers }
}
const parentFolderAuth = parentFolder.auth
const parentFolderHeaders = parentFolder.headers
const parentFolderAuth = parentFolder.auth as HoppRESTAuth | HoppGQLAuth
const parentFolderHeaders = parentFolder.headers as
| HoppRESTHeaders
| HoppGQLHeader[]
// check if the parent folder has authType 'inherit' and if it is the root folder
if (parentFolderAuth?.authType === "inherit" && path.length === 1) {
if (
parentFolderAuth?.authType === "inherit" &&
[...path.slice(0, i + 1)].length === 1
) {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
parentName: parentFolder.name,

View File

@@ -1,11 +1,58 @@
<template>
<div class="flex flex-col flex-1 w-full">
<div class="flex flex-col justify-center">
<div
v-if="invalidLink"
class="flex flex-1 flex-col items-center justify-center p-8"
>
<icon-lucide-alert-triangle class="svg-icons mb-2 opacity-75" />
<h1 class="heading text-center">
{{ t("error.invalid_link") }}
</h1>
<p class="mt-2 text-center">
{{ t("error.invalid_embed_link") }}
</p>
</div>
<Embeds
v-if="tab"
v-else-if="!invalidLink && tab"
v-model:modelTab="tab"
:properties="properties"
:shared-request-i-d="sharedRequestID"
/>
<div v-else class="flex flex-1 flex-col items-center justify-center p-4">
<div
v-if="sharedRequestDetails.loading"
class="flex flex-1 flex-col items-center justify-center p-4"
>
<HoppSmartSpinner />
</div>
<div v-else>
<div
v-if="
!sharedRequestDetails.loading && E.isLeft(sharedRequestDetails.data)
"
class="flex flex-col items-center p-4"
>
<icon-lucide-alert-triangle class="svg-icons mb-2 opacity-75" />
<h1 class="heading text-center">
{{ t("error.invalid_link") }}
</h1>
<p class="mt-2 text-center">
{{ t("error.invalid_embed_link") }}
</p>
</div>
<div
v-if="
!sharedRequestDetails.loading &&
E.isRight(sharedRequestDetails.data)
"
class="flex flex-1 flex-col items-center justify-center p-4"
>
<h1 class="heading">
{{ t("state.loading") }}
</h1>
</div>
</div>
</div>
</div>
</template>
@@ -28,6 +75,9 @@ import {
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { applySetting } from "~/newstore/settings"
import { useI18n } from "~/composables/i18n"
const t = useI18n()
const route = useRoute()

View File

@@ -244,9 +244,13 @@ export class PersistenceService extends Service {
private setupSettingsPersistence() {
const settingsKey = "settings"
let settingsData = JSON.parse(
window.localStorage.getItem(settingsKey) || "{}"
window.localStorage.getItem(settingsKey) ?? "null"
)
if (!settingsData) {
settingsData = getDefaultSettings()
}
// Validate data read from localStorage
const result = SETTINGS_SCHEMA.safeParse(settingsData)
if (result.success) {

View File

@@ -36,7 +36,7 @@ const SettingsDefSchema = z.object({
httpUser: z.boolean(),
httpPassword: z.boolean(),
bearerToken: z.boolean(),
oauth2Token: z.boolean(),
oauth2Token: z.optional(z.boolean()),
}),
THEME_COLOR: ThemeColorSchema,
BG_COLOR: BgColorSchema,
@@ -103,13 +103,10 @@ export const LOCAL_STATE_SCHEMA = z.union([
.strict(),
])
export const SETTINGS_SCHEMA = z.union([
z.object({}).strict(),
SettingsDefSchema.extend({
EXTENSIONS_ENABLED: z.optional(z.boolean()),
PROXY_ENABLED: z.optional(z.boolean()),
}),
])
export const SETTINGS_SCHEMA = SettingsDefSchema.extend({
EXTENSIONS_ENABLED: z.optional(z.boolean()),
PROXY_ENABLED: z.optional(z.boolean()),
})
export const REST_HISTORY_ENTRY_SCHEMA = z
.object({
@@ -208,7 +205,7 @@ export const MQTT_REQUEST_SCHEMA = z.nullable(
z
.object({
endpoint: z.string(),
clientID: z.string(),
clientID: z.optional(z.string()),
})
.strict()
)