refactor: move persistence logic into a dedicated service (#3493)
This commit is contained in:
@@ -57,7 +57,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
name: "localStorage",
|
name: "localStorage",
|
||||||
message:
|
message:
|
||||||
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
|
"Do not use 'localStorage' directly. Please use the PersistenceService",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// window.localStorage block
|
// window.localStorage block
|
||||||
@@ -66,7 +66,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
selector: "CallExpression[callee.object.property.name='localStorage']",
|
selector: "CallExpression[callee.object.property.name='localStorage']",
|
||||||
message:
|
message:
|
||||||
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
|
"Do not use 'localStorage' directly. Please use the PersistenceService",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
"url": "^0.11.1",
|
"url": "^0.11.1",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
|
"verzod": "^0.2.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-pdf-embed": "^1.1.6",
|
"vue-pdf-embed": "^1.1.6",
|
||||||
@@ -143,19 +144,19 @@
|
|||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
"glob": "^10.3.10",
|
"glob": "^10.3.10",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
"openapi-types": "^12.1.3",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.6",
|
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||||
"tailwindcss": "^3.3.2",
|
|
||||||
"vite-plugin-fonts": "^0.6.0",
|
|
||||||
"openapi-types": "^12.1.3",
|
|
||||||
"rollup-plugin-polyfill-node": "^0.12.0",
|
"rollup-plugin-polyfill-node": "^0.12.0",
|
||||||
"sass": "^1.66.0",
|
"sass": "^1.66.0",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"unplugin-fonts": "^1.0.3",
|
"unplugin-fonts": "^1.0.3",
|
||||||
"unplugin-icons": "^0.16.5",
|
"unplugin-icons": "^0.16.5",
|
||||||
"unplugin-vue-components": "^0.25.1",
|
"unplugin-vue-components": "^0.25.1",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-checker": "^0.6.1",
|
"vite-plugin-checker": "^0.6.1",
|
||||||
|
"vite-plugin-fonts": "^0.6.0",
|
||||||
"vite-plugin-html-config": "^1.0.11",
|
"vite-plugin-html-config": "^1.0.11",
|
||||||
"vite-plugin-inspect": "^0.7.38",
|
"vite-plugin-inspect": "^0.7.38",
|
||||||
"vite-plugin-pages": "^0.31.0",
|
"vite-plugin-pages": "^0.31.0",
|
||||||
|
|||||||
@@ -47,14 +47,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Splitpanes, Pane } from "splitpanes"
|
import { Pane, Splitpanes } from "splitpanes"
|
||||||
|
|
||||||
import "splitpanes/dist/splitpanes.css"
|
import "splitpanes/dist/splitpanes.css"
|
||||||
|
|
||||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
|
||||||
import { computed, useSlots, ref } from "vue"
|
|
||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
import { setLocalConfig, getLocalConfig } from "~/newstore/localpersistence"
|
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { computed, ref, useSlots } from "vue"
|
||||||
|
import { PersistenceService } from "~/services/persistence"
|
||||||
|
|
||||||
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
|
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
|
||||||
|
|
||||||
@@ -67,6 +68,8 @@ const SIDEBAR = useSetting("SIDEBAR")
|
|||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
|
||||||
|
const persistenceService = useService(PersistenceService)
|
||||||
|
|
||||||
const hasSidebar = computed(() => !!slots.sidebar)
|
const hasSidebar = computed(() => !!slots.sidebar)
|
||||||
const hasSecondary = computed(() => !!slots.secondary)
|
const hasSecondary = computed(() => !!slots.secondary)
|
||||||
|
|
||||||
@@ -96,7 +99,7 @@ if (!COLUMN_LAYOUT.value) {
|
|||||||
function setPaneEvent(event: PaneEvent[], type: "vertical" | "horizontal") {
|
function setPaneEvent(event: PaneEvent[], type: "vertical" | "horizontal") {
|
||||||
if (!props.layoutId) return
|
if (!props.layoutId) return
|
||||||
const storageKey = `${props.layoutId}-pane-config-${type}`
|
const storageKey = `${props.layoutId}-pane-config-${type}`
|
||||||
setLocalConfig(storageKey, JSON.stringify(event))
|
persistenceService.setLocalConfig(storageKey, JSON.stringify(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
function populatePaneEvent() {
|
function populatePaneEvent() {
|
||||||
@@ -119,7 +122,7 @@ function populatePaneEvent() {
|
|||||||
|
|
||||||
function getPaneData(type: "vertical" | "horizontal"): PaneEvent[] | null {
|
function getPaneData(type: "vertical" | "horizontal"): PaneEvent[] | null {
|
||||||
const storageKey = `${props.layoutId}-pane-config-${type}`
|
const storageKey = `${props.layoutId}-pane-config-${type}`
|
||||||
const paneEvent = getLocalConfig(storageKey)
|
const paneEvent = persistenceService.getLocalConfig(storageKey)
|
||||||
if (!paneEvent) return null
|
if (!paneEvent) return null
|
||||||
return JSON.parse(paneEvent)
|
return JSON.parse(paneEvent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,20 +111,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Ref, computed, onMounted, ref } from "vue"
|
import { Ref, computed, onMounted, ref } from "vue"
|
||||||
|
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useStreamSubscriber } from "@composables/stream"
|
import { useStreamSubscriber } from "@composables/stream"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
|
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { setLocalConfig } from "~/newstore/localpersistence"
|
|
||||||
|
|
||||||
|
import IconEmail from "~icons/auth/email"
|
||||||
import IconGithub from "~icons/auth/github"
|
import IconGithub from "~icons/auth/github"
|
||||||
import IconGoogle from "~icons/auth/google"
|
import IconGoogle from "~icons/auth/google"
|
||||||
import IconEmail from "~icons/auth/email"
|
|
||||||
import IconMicrosoft from "~icons/auth/microsoft"
|
import IconMicrosoft from "~icons/auth/microsoft"
|
||||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||||
|
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
import { LoginItemDef } from "~/platform/auth"
|
import { LoginItemDef } from "~/platform/auth"
|
||||||
|
import { PersistenceService } from "~/services/persistence"
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
@@ -138,6 +139,8 @@ const { subscribeToStream } = useStreamSubscriber()
|
|||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
|
const persistenceService = useService(PersistenceService)
|
||||||
|
|
||||||
const form = {
|
const form = {
|
||||||
email: "",
|
email: "",
|
||||||
}
|
}
|
||||||
@@ -260,7 +263,7 @@ const signInWithEmail = async () => {
|
|||||||
.signInWithEmail(form.email)
|
.signInWithEmail(form.email)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mode.value = "email-sent"
|
mode.value = "email-sent"
|
||||||
setLocalConfig("emailForSignIn", form.email)
|
persistenceService.setLocalConfig("emailForSignIn", form.email)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { completePageProgress, startPageProgress } from "@modules/loadingbar"
|
import { completePageProgress, startPageProgress } from "~/modules/loadingbar"
|
||||||
import * as gql from "graphql"
|
import * as gql from "graphql"
|
||||||
import { clone } from "lodash-es"
|
import { clone } from "lodash-es"
|
||||||
import { computed, ref, watch } from "vue"
|
import { computed, ref, watch } from "vue"
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import {
|
import { getService } from "~/modules/dioc"
|
||||||
getLocalConfig,
|
import { PersistenceService } from "~/services/persistence"
|
||||||
setLocalConfig,
|
|
||||||
removeLocalConfig,
|
|
||||||
} from "~/newstore/localpersistence"
|
|
||||||
|
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
const redirectUri = `${window.location.origin}/oauth`
|
const redirectUri = `${window.location.origin}/oauth`
|
||||||
|
|
||||||
|
const persistenceService = getService(PersistenceService)
|
||||||
|
|
||||||
// GENERAL HELPER FUNCTIONS
|
// GENERAL HELPER FUNCTIONS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,17 +189,17 @@ const tokenRequest = async ({
|
|||||||
accessTokenUrl = parsedOIDCConfiguration.data.token_endpoint
|
accessTokenUrl = parsedOIDCConfiguration.data.token_endpoint
|
||||||
}
|
}
|
||||||
// Store oauth information
|
// Store oauth information
|
||||||
setLocalConfig("tokenEndpoint", accessTokenUrl)
|
persistenceService.setLocalConfig("tokenEndpoint", accessTokenUrl)
|
||||||
setLocalConfig("client_id", clientId)
|
persistenceService.setLocalConfig("client_id", clientId)
|
||||||
setLocalConfig("client_secret", clientSecret)
|
persistenceService.setLocalConfig("client_secret", clientSecret)
|
||||||
|
|
||||||
// Create and store a random state value
|
// Create and store a random state value
|
||||||
const state = generateRandomString()
|
const state = generateRandomString()
|
||||||
setLocalConfig("pkce_state", state)
|
persistenceService.setLocalConfig("pkce_state", state)
|
||||||
|
|
||||||
// Create and store a new PKCE codeVerifier (the plaintext random secret)
|
// Create and store a new PKCE codeVerifier (the plaintext random secret)
|
||||||
const codeVerifier = generateRandomString()
|
const codeVerifier = generateRandomString()
|
||||||
setLocalConfig("pkce_codeVerifier", codeVerifier)
|
persistenceService.setLocalConfig("pkce_codeVerifier", codeVerifier)
|
||||||
|
|
||||||
// Hash and base64-urlencode the secret to use as the challenge
|
// Hash and base64-urlencode the secret to use as the challenge
|
||||||
const codeChallenge = await pkceChallengeFromVerifier(codeVerifier)
|
const codeChallenge = await pkceChallengeFromVerifier(codeVerifier)
|
||||||
@@ -244,14 +243,14 @@ const handleOAuthRedirect = async () => {
|
|||||||
|
|
||||||
// If the server returned an authorization code, attempt to exchange it for an access token
|
// If the server returned an authorization code, attempt to exchange it for an access token
|
||||||
// Verify state matches what we set at the beginning
|
// Verify state matches what we set at the beginning
|
||||||
if (getLocalConfig("pkce_state") !== queryParams.state) {
|
if (persistenceService.getLocalConfig("pkce_state") !== queryParams.state) {
|
||||||
return E.left("INVALID_STATE" as const)
|
return E.left("INVALID_STATE" as const)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenEndpoint = getLocalConfig("tokenEndpoint")
|
const tokenEndpoint = persistenceService.getLocalConfig("tokenEndpoint")
|
||||||
const clientID = getLocalConfig("client_id")
|
const clientID = persistenceService.getLocalConfig("client_id")
|
||||||
const clientSecret = getLocalConfig("client_secret")
|
const clientSecret = persistenceService.getLocalConfig("client_secret")
|
||||||
const codeVerifier = getLocalConfig("pkce_codeVerifier")
|
const codeVerifier = persistenceService.getLocalConfig("pkce_codeVerifier")
|
||||||
|
|
||||||
if (!tokenEndpoint) {
|
if (!tokenEndpoint) {
|
||||||
return E.left("NO_TOKEN_ENDPOINT" as const)
|
return E.left("NO_TOKEN_ENDPOINT" as const)
|
||||||
@@ -303,11 +302,11 @@ const handleOAuthRedirect = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const clearPKCEState = () => {
|
const clearPKCEState = () => {
|
||||||
removeLocalConfig("pkce_state")
|
persistenceService.removeLocalConfig("pkce_state")
|
||||||
removeLocalConfig("pkce_codeVerifier")
|
persistenceService.removeLocalConfig("pkce_codeVerifier")
|
||||||
removeLocalConfig("tokenEndpoint")
|
persistenceService.removeLocalConfig("tokenEndpoint")
|
||||||
removeLocalConfig("client_id")
|
persistenceService.removeLocalConfig("client_id")
|
||||||
removeLocalConfig("client_secret")
|
persistenceService.removeLocalConfig("client_secret")
|
||||||
}
|
}
|
||||||
|
|
||||||
export { tokenRequest, handleOAuthRedirect }
|
export { tokenRequest, handleOAuthRedirect }
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
|
import { HOPP_MODULES } from "@modules/."
|
||||||
import { createApp } from "vue"
|
import { createApp } from "vue"
|
||||||
import { PlatformDef, setPlatformDef } from "./platform"
|
|
||||||
import { setupLocalPersistence } from "./newstore/localpersistence"
|
|
||||||
import { performMigrations } from "./helpers/migrations"
|
|
||||||
import { initializeApp } from "./helpers/app"
|
import { initializeApp } from "./helpers/app"
|
||||||
import { initBackendGQLClient } from "./helpers/backend/GQLClient"
|
import { initBackendGQLClient } from "./helpers/backend/GQLClient"
|
||||||
import { HOPP_MODULES } from "@modules/."
|
import { performMigrations } from "./helpers/migrations"
|
||||||
|
import { PlatformDef, setPlatformDef } from "./platform"
|
||||||
|
|
||||||
import "../assets/scss/tailwind.scss"
|
|
||||||
import "../assets/themes/themes.scss"
|
|
||||||
import "../assets/scss/styles.scss"
|
|
||||||
import "nprogress/nprogress.css"
|
|
||||||
import "@fontsource-variable/inter"
|
import "@fontsource-variable/inter"
|
||||||
import "@fontsource-variable/material-symbols-rounded"
|
import "@fontsource-variable/material-symbols-rounded"
|
||||||
import "@fontsource-variable/roboto-mono"
|
import "@fontsource-variable/roboto-mono"
|
||||||
|
import "nprogress/nprogress.css"
|
||||||
|
import "../assets/scss/styles.scss"
|
||||||
|
import "../assets/scss/tailwind.scss"
|
||||||
|
import "../assets/themes/themes.scss"
|
||||||
|
|
||||||
import App from "./App.vue"
|
import App from "./App.vue"
|
||||||
|
import { getService } from "./modules/dioc"
|
||||||
|
import { PersistenceService } from "./services/persistence"
|
||||||
|
|
||||||
export function createHoppApp(el: string | Element, platformDef: PlatformDef) {
|
export function createHoppApp(el: string | Element, platformDef: PlatformDef) {
|
||||||
setPlatformDef(platformDef)
|
setPlatformDef(platformDef)
|
||||||
@@ -24,12 +25,15 @@ export function createHoppApp(el: string | Element, platformDef: PlatformDef) {
|
|||||||
// Some basic work that needs to be done before module inits even
|
// Some basic work that needs to be done before module inits even
|
||||||
initBackendGQLClient()
|
initBackendGQLClient()
|
||||||
initializeApp()
|
initializeApp()
|
||||||
setupLocalPersistence()
|
|
||||||
performMigrations()
|
|
||||||
|
|
||||||
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app))
|
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app))
|
||||||
platformDef.addedHoppModules?.forEach((mod) => mod.onVueAppInit?.(app))
|
platformDef.addedHoppModules?.forEach((mod) => mod.onVueAppInit?.(app))
|
||||||
|
|
||||||
|
// TODO: Explore possibilities of moving this invocation to the service constructor
|
||||||
|
// `toast` was coming up as `null` in the previous attempts
|
||||||
|
getService(PersistenceService).setupLocalPersistence()
|
||||||
|
performMigrations()
|
||||||
|
|
||||||
app.mount(el)
|
app.mount(el)
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
|
|||||||
@@ -61,19 +61,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeMount, onMounted, ref, watch } from "vue"
|
|
||||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
|
||||||
import { Splitpanes, Pane } from "splitpanes"
|
|
||||||
import "splitpanes/dist/splitpanes.css"
|
|
||||||
import { RouterView, useRouter } from "vue-router"
|
|
||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
|
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { Pane, Splitpanes } from "splitpanes"
|
||||||
|
import "splitpanes/dist/splitpanes.css"
|
||||||
|
import { computed, onBeforeMount, onMounted, ref, watch } from "vue"
|
||||||
|
import { RouterView, useRouter } from "vue-router"
|
||||||
|
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { hookKeybindingsListener } from "~/helpers/keybindings"
|
import { hookKeybindingsListener } from "~/helpers/keybindings"
|
||||||
import { applySetting } from "~/newstore/settings"
|
import { applySetting } from "~/newstore/settings"
|
||||||
import { getLocalConfig, setLocalConfig } from "~/newstore/localpersistence"
|
|
||||||
import { useToast } from "~/composables/toast"
|
import { useToast } from "~/composables/toast"
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { PersistenceService } from "~/services/persistence"
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -90,6 +92,8 @@ const mdAndLarger = breakpoints.greater("md")
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
|
const persistenceService = useService(PersistenceService)
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
if (!mdAndLarger.value) {
|
if (!mdAndLarger.value) {
|
||||||
rightSidebar.value = false
|
rightSidebar.value = false
|
||||||
@@ -98,7 +102,8 @@ onBeforeMount(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const cookiesAllowed = getLocalConfig("cookiesAllowed") === "yes"
|
const cookiesAllowed =
|
||||||
|
persistenceService.getLocalConfig("cookiesAllowed") === "yes"
|
||||||
const platformAllowsCookiePrompts =
|
const platformAllowsCookiePrompts =
|
||||||
platform.platformFeatureFlags.promptAsUsingCookies ?? true
|
platform.platformFeatureFlags.promptAsUsingCookies ?? true
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@ onMounted(() => {
|
|||||||
{
|
{
|
||||||
text: `${t("action.learn_more")}`,
|
text: `${t("action.learn_more")}`,
|
||||||
onClick: (_, toastObject) => {
|
onClick: (_, toastObject) => {
|
||||||
setLocalConfig("cookiesAllowed", "yes")
|
persistenceService.setLocalConfig("cookiesAllowed", "yes")
|
||||||
toastObject.goAway(0)
|
toastObject.goAway(0)
|
||||||
window
|
window
|
||||||
.open("https://docs.hoppscotch.io/support/privacy", "_blank")
|
.open("https://docs.hoppscotch.io/support/privacy", "_blank")
|
||||||
@@ -119,7 +124,7 @@ onMounted(() => {
|
|||||||
{
|
{
|
||||||
text: `${t("action.dismiss")}`,
|
text: `${t("action.dismiss")}`,
|
||||||
onClick: (_, toastObject) => {
|
onClick: (_, toastObject) => {
|
||||||
setLocalConfig("cookiesAllowed", "yes")
|
persistenceService.setLocalConfig("cookiesAllowed", "yes")
|
||||||
toastObject.goAway(0)
|
toastObject.goAway(0)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import * as R from "fp-ts/Record"
|
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import * as O from "fp-ts/Option"
|
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
|
import * as O from "fp-ts/Option"
|
||||||
|
import * as R from "fp-ts/Record"
|
||||||
import { createI18n, I18n, I18nOptions } from "vue-i18n"
|
import { createI18n, I18n, I18nOptions } from "vue-i18n"
|
||||||
import { HoppModule } from "."
|
import { HoppModule } from "."
|
||||||
|
|
||||||
import languages from "../../languages.json"
|
import languages from "../../languages.json"
|
||||||
|
|
||||||
import { throwError } from "~/helpers/functional/error"
|
import { throwError } from "~/helpers/functional/error"
|
||||||
import { getLocalConfig, setLocalConfig } from "~/newstore/localpersistence"
|
import { PersistenceService } from "~/services/persistence"
|
||||||
|
import { getService } from "./dioc"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
In context of this file, we have 2 main kinds of things.
|
In context of this file, we have 2 main kinds of things.
|
||||||
@@ -44,6 +45,8 @@ type LanguagesDef = {
|
|||||||
|
|
||||||
const FALLBACK_LANG_CODE = "en"
|
const FALLBACK_LANG_CODE = "en"
|
||||||
|
|
||||||
|
const persistenceService = getService(PersistenceService)
|
||||||
|
|
||||||
// TypeScript cannot understand dir is restricted to "ltr" or "rtl" yet, hence assertion
|
// TypeScript cannot understand dir is restricted to "ltr" or "rtl" yet, hence assertion
|
||||||
export const APP_LANGUAGES: LanguagesDef[] = languages as LanguagesDef[]
|
export const APP_LANGUAGES: LanguagesDef[] = languages as LanguagesDef[]
|
||||||
|
|
||||||
@@ -69,7 +72,7 @@ let i18nInstance: I18n<
|
|||||||
const resolveCurrentLocale = () =>
|
const resolveCurrentLocale = () =>
|
||||||
pipe(
|
pipe(
|
||||||
// Resolve from locale and make sure it is in languages
|
// Resolve from locale and make sure it is in languages
|
||||||
getLocalConfig("locale"),
|
persistenceService.getLocalConfig("locale"),
|
||||||
O.fromNullable,
|
O.fromNullable,
|
||||||
O.filter((locale) =>
|
O.filter((locale) =>
|
||||||
pipe(
|
pipe(
|
||||||
@@ -118,7 +121,7 @@ export const changeAppLanguage = async (locale: string) => {
|
|||||||
// TODO: Look into the type issues here
|
// TODO: Look into the type issues here
|
||||||
i18nInstance.global.locale.value = locale
|
i18nInstance.global.locale.value = locale
|
||||||
|
|
||||||
setLocalConfig("locale", locale)
|
persistenceService.setLocalConfig("locale", locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,7 +148,7 @@ export default <HoppModule>{
|
|||||||
const currentLocale = resolveCurrentLocale()
|
const currentLocale = resolveCurrentLocale()
|
||||||
changeAppLanguage(currentLocale)
|
changeAppLanguage(currentLocale)
|
||||||
|
|
||||||
setLocalConfig("locale", currentLocale)
|
persistenceService.setLocalConfig("locale", currentLocale)
|
||||||
},
|
},
|
||||||
onBeforeRouteChange(to, _, router) {
|
onBeforeRouteChange(to, _, router) {
|
||||||
// Convert old locale path format to new format
|
// Convert old locale path format to new format
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
import { usePreferredDark, useStorage } from "@vueuse/core"
|
|
||||||
import { App, computed, reactive, Ref, watch } from "vue"
|
|
||||||
import type { HoppBgColor } from "~/newstore/settings"
|
|
||||||
import { useSettingStatic } from "@composables/settings"
|
import { useSettingStatic } from "@composables/settings"
|
||||||
|
import { usePreferredDark, useStorage } from "@vueuse/core"
|
||||||
|
import { App, Ref, computed, reactive, watch } from "vue"
|
||||||
|
|
||||||
|
import type { HoppBgColor } from "~/newstore/settings"
|
||||||
|
import { PersistenceService } from "~/services/persistence"
|
||||||
import { HoppModule } from "."
|
import { HoppModule } from "."
|
||||||
import { hoppLocalConfigStorage } from "~/newstore/localpersistence"
|
import { getService } from "./dioc"
|
||||||
|
|
||||||
export type HoppColorMode = {
|
export type HoppColorMode = {
|
||||||
preference: HoppBgColor
|
preference: HoppBgColor
|
||||||
value: Readonly<Exclude<HoppBgColor, "system">>
|
value: Readonly<Exclude<HoppBgColor, "system">>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const persistenceService = getService(PersistenceService)
|
||||||
|
|
||||||
const applyColorMode = (app: App) => {
|
const applyColorMode = (app: App) => {
|
||||||
const [settingPref] = useSettingStatic("BG_COLOR")
|
const [settingPref] = useSettingStatic("BG_COLOR")
|
||||||
|
|
||||||
const currentLocalPreference = useStorage<HoppBgColor>(
|
const currentLocalPreference = useStorage<HoppBgColor>(
|
||||||
"nuxt-color-mode",
|
"nuxt-color-mode",
|
||||||
"system",
|
"system",
|
||||||
hoppLocalConfigStorage,
|
persistenceService.hoppLocalConfigStorage,
|
||||||
{
|
{
|
||||||
listenToStorageChanges: true,
|
listenToStorageChanges: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,457 +0,0 @@
|
|||||||
/* eslint-disable no-restricted-globals, no-restricted-syntax */
|
|
||||||
|
|
||||||
import { clone, assign, isEmpty } from "lodash-es"
|
|
||||||
import {
|
|
||||||
translateToNewRESTCollection,
|
|
||||||
translateToNewGQLCollection,
|
|
||||||
Environment,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import {
|
|
||||||
settingsStore,
|
|
||||||
bulkApplySettings,
|
|
||||||
getDefaultSettings,
|
|
||||||
applySetting,
|
|
||||||
HoppAccentColor,
|
|
||||||
HoppBgColor,
|
|
||||||
performSettingsDataMigrations,
|
|
||||||
} from "./settings"
|
|
||||||
import {
|
|
||||||
restHistoryStore,
|
|
||||||
graphqlHistoryStore,
|
|
||||||
setRESTHistoryEntries,
|
|
||||||
setGraphqlHistoryEntries,
|
|
||||||
translateToNewRESTHistory,
|
|
||||||
translateToNewGQLHistory,
|
|
||||||
} from "./history"
|
|
||||||
import {
|
|
||||||
restCollectionStore,
|
|
||||||
graphqlCollectionStore,
|
|
||||||
setGraphqlCollections,
|
|
||||||
setRESTCollections,
|
|
||||||
} from "./collections"
|
|
||||||
import {
|
|
||||||
replaceEnvironments,
|
|
||||||
environments$,
|
|
||||||
addGlobalEnvVariable,
|
|
||||||
setGlobalEnvVariables,
|
|
||||||
globalEnv$,
|
|
||||||
setSelectedEnvironmentIndex,
|
|
||||||
selectedEnvironmentIndex$,
|
|
||||||
} from "./environments"
|
|
||||||
import { WSRequest$, setWSRequest } from "./WebSocketSession"
|
|
||||||
import { SIORequest$, setSIORequest } from "./SocketIOSession"
|
|
||||||
import { SSERequest$, setSSERequest } from "./SSESession"
|
|
||||||
import { MQTTRequest$, setMQTTRequest } from "./MQTTSession"
|
|
||||||
import { bulkApplyLocalState, localStateStore } from "./localstate"
|
|
||||||
import { StorageLike, watchDebounced } from "@vueuse/core"
|
|
||||||
import { getService } from "~/modules/dioc"
|
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
|
||||||
import { GQLTabService } from "~/services/tab/graphql"
|
|
||||||
import { z } from "zod"
|
|
||||||
import { CookieJarService } from "~/services/cookie-jar.service"
|
|
||||||
import { watch } from "vue"
|
|
||||||
|
|
||||||
function checkAndMigrateOldSettings() {
|
|
||||||
if (window.localStorage.getItem("selectedEnvIndex")) {
|
|
||||||
const index = window.localStorage.getItem("selectedEnvIndex")
|
|
||||||
if (index) {
|
|
||||||
if (index === "-1") {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
"selectedEnvIndex",
|
|
||||||
JSON.stringify({
|
|
||||||
type: "NO_ENV_SELECTED",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else if (Number(index) >= 0) {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
"selectedEnvIndex",
|
|
||||||
JSON.stringify({
|
|
||||||
type: "MY_ENV",
|
|
||||||
index: parseInt(index),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const vuexData = JSON.parse(window.localStorage.getItem("vuex") || "{}")
|
|
||||||
|
|
||||||
if (isEmpty(vuexData)) return
|
|
||||||
|
|
||||||
const { postwoman } = vuexData
|
|
||||||
|
|
||||||
if (!isEmpty(postwoman?.settings)) {
|
|
||||||
const settingsData = assign(clone(getDefaultSettings()), postwoman.settings)
|
|
||||||
|
|
||||||
window.localStorage.setItem("settings", JSON.stringify(settingsData))
|
|
||||||
|
|
||||||
delete postwoman.settings
|
|
||||||
window.localStorage.setItem("vuex", JSON.stringify(vuexData))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postwoman?.collections) {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
"collections",
|
|
||||||
JSON.stringify(postwoman.collections)
|
|
||||||
)
|
|
||||||
|
|
||||||
delete postwoman.collections
|
|
||||||
window.localStorage.setItem("vuex", JSON.stringify(vuexData))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postwoman?.collectionsGraphql) {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
"collectionsGraphql",
|
|
||||||
JSON.stringify(postwoman.collectionsGraphql)
|
|
||||||
)
|
|
||||||
|
|
||||||
delete postwoman.collectionsGraphql
|
|
||||||
window.localStorage.setItem("vuex", JSON.stringify(vuexData))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postwoman?.environments) {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
"environments",
|
|
||||||
JSON.stringify(postwoman.environments)
|
|
||||||
)
|
|
||||||
|
|
||||||
delete postwoman.environments
|
|
||||||
window.localStorage.setItem("vuex", JSON.stringify(vuexData))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.localStorage.getItem("THEME_COLOR")) {
|
|
||||||
const themeColor = window.localStorage.getItem("THEME_COLOR")
|
|
||||||
applySetting("THEME_COLOR", themeColor as HoppAccentColor)
|
|
||||||
|
|
||||||
window.localStorage.removeItem("THEME_COLOR")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.localStorage.getItem("nuxt-color-mode")) {
|
|
||||||
const color = window.localStorage.getItem("nuxt-color-mode") as HoppBgColor
|
|
||||||
applySetting("BG_COLOR", color)
|
|
||||||
|
|
||||||
window.localStorage.removeItem("nuxt-color-mode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupLocalStatePersistence() {
|
|
||||||
const localStateData = JSON.parse(
|
|
||||||
window.localStorage.getItem("localState") ?? "{}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (localStateData) bulkApplyLocalState(localStateData)
|
|
||||||
|
|
||||||
localStateStore.subject$.subscribe((state) => {
|
|
||||||
window.localStorage.setItem("localState", JSON.stringify(state))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupSettingsPersistence() {
|
|
||||||
const settingsData = JSON.parse(
|
|
||||||
window.localStorage.getItem("settings") || "{}"
|
|
||||||
)
|
|
||||||
|
|
||||||
const updatedSettings = settingsData
|
|
||||||
? performSettingsDataMigrations(settingsData)
|
|
||||||
: settingsData
|
|
||||||
|
|
||||||
if (updatedSettings) {
|
|
||||||
bulkApplySettings(updatedSettings)
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsStore.subject$.subscribe((settings) => {
|
|
||||||
window.localStorage.setItem("settings", JSON.stringify(settings))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupHistoryPersistence() {
|
|
||||||
const restHistoryData = JSON.parse(
|
|
||||||
window.localStorage.getItem("history") || "[]"
|
|
||||||
).map(translateToNewRESTHistory)
|
|
||||||
|
|
||||||
const graphqlHistoryData = JSON.parse(
|
|
||||||
window.localStorage.getItem("graphqlHistory") || "[]"
|
|
||||||
).map(translateToNewGQLHistory)
|
|
||||||
|
|
||||||
setRESTHistoryEntries(restHistoryData)
|
|
||||||
setGraphqlHistoryEntries(graphqlHistoryData)
|
|
||||||
|
|
||||||
restHistoryStore.subject$.subscribe(({ state }) => {
|
|
||||||
window.localStorage.setItem("history", JSON.stringify(state))
|
|
||||||
})
|
|
||||||
|
|
||||||
graphqlHistoryStore.subject$.subscribe(({ state }) => {
|
|
||||||
window.localStorage.setItem("graphqlHistory", JSON.stringify(state))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookieSchema = z.record(z.array(z.string()))
|
|
||||||
|
|
||||||
function setupCookiesPersistence() {
|
|
||||||
const cookieJarService = getService(CookieJarService)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cookieData = JSON.parse(
|
|
||||||
window.localStorage.getItem("cookieJar") || "{}"
|
|
||||||
)
|
|
||||||
|
|
||||||
const parseResult = cookieSchema.safeParse(cookieData)
|
|
||||||
|
|
||||||
if (parseResult.success) {
|
|
||||||
for (const domain in parseResult.data) {
|
|
||||||
cookieJarService.bulkApplyCookiesToDomain(
|
|
||||||
parseResult.data[domain],
|
|
||||||
domain
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
watch(cookieJarService.cookieJar, (cookieJar) => {
|
|
||||||
const data = JSON.stringify(Object.fromEntries(cookieJar.entries()))
|
|
||||||
|
|
||||||
window.localStorage.setItem("cookieJar", data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupCollectionsPersistence() {
|
|
||||||
const restCollectionData = JSON.parse(
|
|
||||||
window.localStorage.getItem("collections") || "[]"
|
|
||||||
).map(translateToNewRESTCollection)
|
|
||||||
|
|
||||||
const graphqlCollectionData = JSON.parse(
|
|
||||||
window.localStorage.getItem("collectionsGraphql") || "[]"
|
|
||||||
).map(translateToNewGQLCollection)
|
|
||||||
|
|
||||||
setRESTCollections(restCollectionData)
|
|
||||||
setGraphqlCollections(graphqlCollectionData)
|
|
||||||
|
|
||||||
restCollectionStore.subject$.subscribe(({ state }) => {
|
|
||||||
window.localStorage.setItem("collections", JSON.stringify(state))
|
|
||||||
})
|
|
||||||
|
|
||||||
graphqlCollectionStore.subject$.subscribe(({ state }) => {
|
|
||||||
window.localStorage.setItem("collectionsGraphql", JSON.stringify(state))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupEnvironmentsPersistence() {
|
|
||||||
const environmentsData: Environment[] = JSON.parse(
|
|
||||||
window.localStorage.getItem("environments") || "[]"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if a global env is defined and if so move that to globals
|
|
||||||
const globalIndex = environmentsData.findIndex(
|
|
||||||
(x) => x.name.toLowerCase() === "globals"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (globalIndex !== -1) {
|
|
||||||
const globalEnv = environmentsData[globalIndex]
|
|
||||||
globalEnv.variables.forEach((variable) => addGlobalEnvVariable(variable))
|
|
||||||
|
|
||||||
// Remove global from environments
|
|
||||||
environmentsData.splice(globalIndex, 1)
|
|
||||||
|
|
||||||
// Just sync the changes manually
|
|
||||||
window.localStorage.setItem(
|
|
||||||
"environments",
|
|
||||||
JSON.stringify(environmentsData)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceEnvironments(environmentsData)
|
|
||||||
|
|
||||||
environments$.subscribe((envs) => {
|
|
||||||
window.localStorage.setItem("environments", JSON.stringify(envs))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupSelectedEnvPersistence() {
|
|
||||||
const selectedEnvIndex = JSON.parse(
|
|
||||||
window.localStorage.getItem("selectedEnvIndex") ?? "null"
|
|
||||||
)
|
|
||||||
|
|
||||||
// If there is a selected env index, set it to the store else set it to null
|
|
||||||
if (selectedEnvIndex) {
|
|
||||||
setSelectedEnvironmentIndex(selectedEnvIndex)
|
|
||||||
} else {
|
|
||||||
setSelectedEnvironmentIndex({
|
|
||||||
type: "NO_ENV_SELECTED",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedEnvironmentIndex$.subscribe((envIndex) => {
|
|
||||||
window.localStorage.setItem("selectedEnvIndex", JSON.stringify(envIndex))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupWebsocketPersistence() {
|
|
||||||
const request = JSON.parse(
|
|
||||||
window.localStorage.getItem("WebsocketRequest") || "null"
|
|
||||||
)
|
|
||||||
|
|
||||||
setWSRequest(request)
|
|
||||||
|
|
||||||
WSRequest$.subscribe((req) => {
|
|
||||||
window.localStorage.setItem("WebsocketRequest", JSON.stringify(req))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupSocketIOPersistence() {
|
|
||||||
const request = JSON.parse(
|
|
||||||
window.localStorage.getItem("SocketIORequest") || "null"
|
|
||||||
)
|
|
||||||
|
|
||||||
setSIORequest(request)
|
|
||||||
|
|
||||||
SIORequest$.subscribe((req) => {
|
|
||||||
window.localStorage.setItem("SocketIORequest", JSON.stringify(req))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupSSEPersistence() {
|
|
||||||
const request = JSON.parse(
|
|
||||||
window.localStorage.getItem("SSERequest") || "null"
|
|
||||||
)
|
|
||||||
|
|
||||||
setSSERequest(request)
|
|
||||||
|
|
||||||
SSERequest$.subscribe((req) => {
|
|
||||||
window.localStorage.setItem("SSERequest", JSON.stringify(req))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupMQTTPersistence() {
|
|
||||||
const request = JSON.parse(
|
|
||||||
window.localStorage.getItem("MQTTRequest") || "null"
|
|
||||||
)
|
|
||||||
|
|
||||||
setMQTTRequest(request)
|
|
||||||
|
|
||||||
MQTTRequest$.subscribe((req) => {
|
|
||||||
window.localStorage.setItem("MQTTRequest", JSON.stringify(req))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupGlobalEnvsPersistence() {
|
|
||||||
const globals: Environment["variables"] = JSON.parse(
|
|
||||||
window.localStorage.getItem("globalEnv") || "[]"
|
|
||||||
)
|
|
||||||
|
|
||||||
setGlobalEnvVariables(globals)
|
|
||||||
|
|
||||||
globalEnv$.subscribe((vars) => {
|
|
||||||
window.localStorage.setItem("globalEnv", JSON.stringify(vars))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Graceful error handling ?
|
|
||||||
export function setupRESTTabsPersistence() {
|
|
||||||
const tabService = getService(RESTTabService)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const state = window.localStorage.getItem("restTabState")
|
|
||||||
if (state) {
|
|
||||||
const data = JSON.parse(state)
|
|
||||||
tabService.loadTabsFromPersistedState(data)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(
|
|
||||||
`Failed parsing persisted tab state, state:`,
|
|
||||||
window.localStorage.getItem("restTabState")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
watchDebounced(
|
|
||||||
tabService.persistableTabState,
|
|
||||||
(state) => {
|
|
||||||
window.localStorage.setItem("restTabState", JSON.stringify(state))
|
|
||||||
},
|
|
||||||
{ debounce: 500, deep: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupGQLTabsPersistence() {
|
|
||||||
const tabService = getService(GQLTabService)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const state = window.localStorage.getItem("gqlTabState")
|
|
||||||
if (state) {
|
|
||||||
const data = JSON.parse(state)
|
|
||||||
tabService.loadTabsFromPersistedState(data)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(
|
|
||||||
`Failed parsing persisted tab state, state:`,
|
|
||||||
window.localStorage.getItem("gqlTabState")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
watchDebounced(
|
|
||||||
tabService.persistableTabState,
|
|
||||||
(state) => {
|
|
||||||
window.localStorage.setItem("gqlTabState", JSON.stringify(state))
|
|
||||||
},
|
|
||||||
{ debounce: 500, deep: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupLocalPersistence() {
|
|
||||||
checkAndMigrateOldSettings()
|
|
||||||
|
|
||||||
setupLocalStatePersistence()
|
|
||||||
setupSettingsPersistence()
|
|
||||||
setupRESTTabsPersistence()
|
|
||||||
|
|
||||||
setupGQLTabsPersistence()
|
|
||||||
|
|
||||||
setupHistoryPersistence()
|
|
||||||
setupCollectionsPersistence()
|
|
||||||
setupGlobalEnvsPersistence()
|
|
||||||
setupEnvironmentsPersistence()
|
|
||||||
setupSelectedEnvPersistence()
|
|
||||||
setupWebsocketPersistence()
|
|
||||||
setupSocketIOPersistence()
|
|
||||||
setupSSEPersistence()
|
|
||||||
setupMQTTPersistence()
|
|
||||||
|
|
||||||
setupCookiesPersistence()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a value in LocalStorage.
|
|
||||||
*
|
|
||||||
* NOTE: Use LocalStorage to only store non-reactive simple data
|
|
||||||
* For more complex data, use stores and connect it to localpersistence
|
|
||||||
*/
|
|
||||||
export function getLocalConfig(name: string) {
|
|
||||||
return window.localStorage.getItem(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a value in LocalStorage.
|
|
||||||
*
|
|
||||||
* NOTE: Use LocalStorage to only store non-reactive simple data
|
|
||||||
* For more complex data, use stores and connect it to localpersistence
|
|
||||||
*/
|
|
||||||
export function setLocalConfig(key: string, value: string) {
|
|
||||||
window.localStorage.setItem(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear config value in LocalStorage.
|
|
||||||
* @param key Key to be cleared
|
|
||||||
*/
|
|
||||||
export function removeLocalConfig(key: string) {
|
|
||||||
window.localStorage.removeItem(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The storage system we are using in the application.
|
|
||||||
* NOTE: This is a placeholder for being used in app.
|
|
||||||
* This entire redirection of localStorage is to allow for
|
|
||||||
* not refactoring the entire app code when we refactor when
|
|
||||||
* we are building the native (which may lack localStorage,
|
|
||||||
* or use a custom system)
|
|
||||||
*/
|
|
||||||
export const hoppLocalConfigStorage: StorageLike = localStorage
|
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import {
|
||||||
|
Environment,
|
||||||
|
HoppCollection,
|
||||||
|
HoppGQLRequest,
|
||||||
|
HoppRESTRequest,
|
||||||
|
} from "@hoppscotch/data"
|
||||||
|
|
||||||
|
import { HoppGQLDocument } from "~/helpers/graphql/document"
|
||||||
|
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||||
|
import { GQLHistoryEntry, RESTHistoryEntry } from "~/newstore/history"
|
||||||
|
import { SettingsDef, getDefaultSettings } from "~/newstore/settings"
|
||||||
|
import { PersistableTabState } from "~/services/tab"
|
||||||
|
|
||||||
|
type VUEX_DATA = {
|
||||||
|
postwoman: {
|
||||||
|
settings?: SettingsDef
|
||||||
|
collections?: HoppCollection<HoppRESTRequest>[]
|
||||||
|
collectionsGraphql?: HoppCollection<HoppGQLRequest>[]
|
||||||
|
environments?: Environment[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS = getDefaultSettings()
|
||||||
|
|
||||||
|
export const REST_COLLECTIONS_MOCK: HoppCollection<HoppRESTRequest>[] = [
|
||||||
|
{
|
||||||
|
v: 1,
|
||||||
|
name: "Echo",
|
||||||
|
folders: [],
|
||||||
|
requests: [
|
||||||
|
{
|
||||||
|
v: "1",
|
||||||
|
endpoint: "https://echo.hoppscotch.io",
|
||||||
|
name: "Echo test",
|
||||||
|
params: [],
|
||||||
|
headers: [],
|
||||||
|
method: "GET",
|
||||||
|
auth: { authType: "none", authActive: true },
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
|
body: { contentType: null, body: null },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const GQL_COLLECTIONS_MOCK: HoppCollection<HoppGQLRequest>[] = [
|
||||||
|
{
|
||||||
|
v: 1,
|
||||||
|
name: "Echo",
|
||||||
|
folders: [],
|
||||||
|
requests: [
|
||||||
|
{
|
||||||
|
v: 2,
|
||||||
|
name: "Echo test",
|
||||||
|
url: "https://echo.hoppscotch.io/graphql",
|
||||||
|
headers: [],
|
||||||
|
variables: '{\n "id": "1"\n}',
|
||||||
|
query: "query Request { url }",
|
||||||
|
auth: { authType: "none", authActive: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const ENVIRONMENTS_MOCK: Environment[] = [
|
||||||
|
{
|
||||||
|
name: "globals",
|
||||||
|
variables: [
|
||||||
|
{
|
||||||
|
key: "test-global-key",
|
||||||
|
value: "test-global-value",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: "Test", variables: [{ key: "test-key", value: "test-value" }] },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const SELECTED_ENV_INDEX_MOCK = {
|
||||||
|
type: "MY_ENV",
|
||||||
|
index: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WEBSOCKET_REQUEST_MOCK = {
|
||||||
|
endpoint: "wss://echo-websocket.hoppscotch.io",
|
||||||
|
protocols: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SOCKET_IO_REQUEST_MOCK = {
|
||||||
|
endpoint: "wss://echo-socketio.hoppscotch.io",
|
||||||
|
path: "/socket.io",
|
||||||
|
version: "v4",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SSE_REQUEST_MOCK = {
|
||||||
|
endpoint: "https://express-eventsource.herokuapp.com/events",
|
||||||
|
eventType: "data",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MQTT_REQUEST_MOCK = {
|
||||||
|
endpoint: "wss://test.mosquitto.org:8081",
|
||||||
|
clientID: "hoppscotch",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GLOBAL_ENV_MOCK: Environment["variables"] = [
|
||||||
|
{ key: "test-key", value: "test-value" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const VUEX_DATA_MOCK: VUEX_DATA = {
|
||||||
|
postwoman: {
|
||||||
|
settings: { ...DEFAULT_SETTINGS, THEME_COLOR: "purple" },
|
||||||
|
collections: REST_COLLECTIONS_MOCK,
|
||||||
|
collectionsGraphql: GQL_COLLECTIONS_MOCK,
|
||||||
|
environments: ENVIRONMENTS_MOCK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const REST_HISTORY_MOCK: RESTHistoryEntry[] = [
|
||||||
|
{
|
||||||
|
v: 1,
|
||||||
|
request: {
|
||||||
|
auth: { authType: "none", authActive: true },
|
||||||
|
body: { contentType: null, body: null },
|
||||||
|
endpoint: "https://echo.hoppscotch.io",
|
||||||
|
headers: [],
|
||||||
|
method: "GET",
|
||||||
|
name: "Untitled",
|
||||||
|
params: [],
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
|
v: "1",
|
||||||
|
},
|
||||||
|
responseMeta: { duration: 807, statusCode: 200 },
|
||||||
|
star: false,
|
||||||
|
updatedOn: new Date("2023-11-07T05:27:32.951Z"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const GQL_HISTORY_MOCK: GQLHistoryEntry[] = [
|
||||||
|
{
|
||||||
|
v: 1,
|
||||||
|
request: {
|
||||||
|
v: 2,
|
||||||
|
name: "Untitled",
|
||||||
|
url: "https://echo.hoppscotch.io/graphql",
|
||||||
|
query: "query Request { url }",
|
||||||
|
headers: [],
|
||||||
|
variables: "",
|
||||||
|
auth: { authType: "none", authActive: true },
|
||||||
|
},
|
||||||
|
response: '{"data":{"url":"/graphql"}}',
|
||||||
|
star: false,
|
||||||
|
updatedOn: new Date("2023-11-07T05:28:21.073Z"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const GQL_TAB_STATE_MOCK: PersistableTabState<HoppGQLDocument> = {
|
||||||
|
lastActiveTabID: "5edbe8d4-65c9-4381-9354-5f1bf05d8ccc",
|
||||||
|
orderedDocs: [
|
||||||
|
{
|
||||||
|
tabID: "5edbe8d4-65c9-4381-9354-5f1bf05d8ccc",
|
||||||
|
doc: {
|
||||||
|
request: {
|
||||||
|
v: 2,
|
||||||
|
name: "Untitled",
|
||||||
|
url: "https://echo.hoppscotch.io/graphql",
|
||||||
|
headers: [],
|
||||||
|
variables: '{\n "id": "1"\n}',
|
||||||
|
query: "query Request { url }",
|
||||||
|
auth: { authType: "none", authActive: true },
|
||||||
|
},
|
||||||
|
isDirty: true,
|
||||||
|
optionTabPreference: "query",
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRESTDocument> = {
|
||||||
|
lastActiveTabID: "e6e8d800-caa8-44a2-a6a6-b4765a3167aa",
|
||||||
|
orderedDocs: [
|
||||||
|
{
|
||||||
|
tabID: "e6e8d800-caa8-44a2-a6a6-b4765a3167aa",
|
||||||
|
doc: {
|
||||||
|
request: {
|
||||||
|
v: "1",
|
||||||
|
endpoint: "https://echo.hoppscotch.io",
|
||||||
|
name: "Echo test",
|
||||||
|
params: [],
|
||||||
|
headers: [],
|
||||||
|
method: "GET",
|
||||||
|
auth: { authType: "none", authActive: true },
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
|
body: { contentType: null, body: null },
|
||||||
|
},
|
||||||
|
isDirty: false,
|
||||||
|
saveContext: {
|
||||||
|
originLocation: "user-collection",
|
||||||
|
folderPath: "0",
|
||||||
|
requestIndex: 0,
|
||||||
|
},
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
724
packages/hoppscotch-common/src/services/persistence/index.ts
Normal file
724
packages/hoppscotch-common/src/services/persistence/index.ts
Normal file
@@ -0,0 +1,724 @@
|
|||||||
|
/* eslint-disable no-restricted-globals, no-restricted-syntax */
|
||||||
|
|
||||||
|
import {
|
||||||
|
Environment,
|
||||||
|
translateToNewGQLCollection,
|
||||||
|
translateToNewRESTCollection,
|
||||||
|
} from "@hoppscotch/data"
|
||||||
|
import { StorageLike, watchDebounced } from "@vueuse/core"
|
||||||
|
import { Service } from "dioc"
|
||||||
|
import { assign, clone, isEmpty } from "lodash-es"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { GQLTabService } from "~/services/tab/graphql"
|
||||||
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
import { MQTTRequest$, setMQTTRequest } from "../../newstore/MQTTSession"
|
||||||
|
import { SSERequest$, setSSERequest } from "../../newstore/SSESession"
|
||||||
|
import { SIORequest$, setSIORequest } from "../../newstore/SocketIOSession"
|
||||||
|
import { WSRequest$, setWSRequest } from "../../newstore/WebSocketSession"
|
||||||
|
import {
|
||||||
|
graphqlCollectionStore,
|
||||||
|
restCollectionStore,
|
||||||
|
setGraphqlCollections,
|
||||||
|
setRESTCollections,
|
||||||
|
} from "../../newstore/collections"
|
||||||
|
import {
|
||||||
|
addGlobalEnvVariable,
|
||||||
|
environments$,
|
||||||
|
globalEnv$,
|
||||||
|
replaceEnvironments,
|
||||||
|
selectedEnvironmentIndex$,
|
||||||
|
setGlobalEnvVariables,
|
||||||
|
setSelectedEnvironmentIndex,
|
||||||
|
} from "../../newstore/environments"
|
||||||
|
import {
|
||||||
|
graphqlHistoryStore,
|
||||||
|
restHistoryStore,
|
||||||
|
setGraphqlHistoryEntries,
|
||||||
|
setRESTHistoryEntries,
|
||||||
|
translateToNewGQLHistory,
|
||||||
|
translateToNewRESTHistory,
|
||||||
|
} from "../../newstore/history"
|
||||||
|
import { bulkApplyLocalState, localStateStore } from "../../newstore/localstate"
|
||||||
|
import {
|
||||||
|
HoppAccentColor,
|
||||||
|
HoppBgColor,
|
||||||
|
applySetting,
|
||||||
|
bulkApplySettings,
|
||||||
|
getDefaultSettings,
|
||||||
|
performSettingsDataMigrations,
|
||||||
|
settingsStore,
|
||||||
|
} from "../../newstore/settings"
|
||||||
|
import {
|
||||||
|
ENVIRONMENTS_SCHEMA,
|
||||||
|
GLOBAL_ENV_SCHEMA,
|
||||||
|
GQL_COLLECTION_SCHEMA,
|
||||||
|
GQL_HISTORY_ENTRY_SCHEMA,
|
||||||
|
GQL_TAB_STATE_SCHEMA,
|
||||||
|
LOCAL_STATE_SCHEMA,
|
||||||
|
MQTT_REQUEST_SCHEMA,
|
||||||
|
NUXT_COLOR_MODE_SCHEMA,
|
||||||
|
REST_COLLECTION_SCHEMA,
|
||||||
|
REST_HISTORY_ENTRY_SCHEMA,
|
||||||
|
REST_TAB_STATE_SCHEMA,
|
||||||
|
SELECTED_ENV_INDEX_SCHEMA,
|
||||||
|
SETTINGS_SCHEMA,
|
||||||
|
SOCKET_IO_REQUEST_SCHEMA,
|
||||||
|
SSE_REQUEST_SCHEMA,
|
||||||
|
THEME_COLOR_SCHEMA,
|
||||||
|
VUEX_SCHEMA,
|
||||||
|
WEBSOCKET_REQUEST_SCHEMA,
|
||||||
|
} from "./validation-schemas"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This service compiles persistence logic across the codebase
|
||||||
|
*/
|
||||||
|
export class PersistenceService extends Service {
|
||||||
|
public static readonly ID = "PERSISTENCE_SERVICE"
|
||||||
|
|
||||||
|
private readonly restTabService = this.bind(RESTTabService)
|
||||||
|
private readonly gqlTabService = this.bind(GQLTabService)
|
||||||
|
|
||||||
|
public hoppLocalConfigStorage: StorageLike = localStorage
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
private showErrorToast(localStorageKey: string) {
|
||||||
|
const toast = useToast()
|
||||||
|
toast.error(
|
||||||
|
`There's a mismatch with the expected schema for the value corresponding to ${localStorageKey} read from localStorage, keeping a backup in ${localStorageKey}-backup`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkAndMigrateOldSettings() {
|
||||||
|
if (window.localStorage.getItem("selectedEnvIndex")) {
|
||||||
|
const index = window.localStorage.getItem("selectedEnvIndex")
|
||||||
|
if (index) {
|
||||||
|
if (index === "-1") {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
"selectedEnvIndex",
|
||||||
|
JSON.stringify({
|
||||||
|
type: "NO_ENV_SELECTED",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else if (Number(index) >= 0) {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
"selectedEnvIndex",
|
||||||
|
JSON.stringify({
|
||||||
|
type: "MY_ENV",
|
||||||
|
index: parseInt(index),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const vuexKey = "vuex"
|
||||||
|
let vuexData = JSON.parse(window.localStorage.getItem(vuexKey) || "{}")
|
||||||
|
|
||||||
|
if (isEmpty(vuexData)) return
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = VUEX_SCHEMA.safeParse(vuexData)
|
||||||
|
if (result.success) {
|
||||||
|
vuexData = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(vuexKey)
|
||||||
|
window.localStorage.setItem(`${vuexKey}-backup`, JSON.stringify(vuexData))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { postwoman } = vuexData
|
||||||
|
|
||||||
|
if (!isEmpty(postwoman?.settings)) {
|
||||||
|
const settingsData = assign(
|
||||||
|
clone(getDefaultSettings()),
|
||||||
|
postwoman.settings
|
||||||
|
)
|
||||||
|
|
||||||
|
window.localStorage.setItem("settings", JSON.stringify(settingsData))
|
||||||
|
|
||||||
|
delete postwoman.settings
|
||||||
|
window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postwoman?.collections) {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
"collections",
|
||||||
|
JSON.stringify(postwoman.collections)
|
||||||
|
)
|
||||||
|
|
||||||
|
delete postwoman.collections
|
||||||
|
window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postwoman?.collectionsGraphql) {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
"collectionsGraphql",
|
||||||
|
JSON.stringify(postwoman.collectionsGraphql)
|
||||||
|
)
|
||||||
|
|
||||||
|
delete postwoman.collectionsGraphql
|
||||||
|
window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postwoman?.environments) {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
"environments",
|
||||||
|
JSON.stringify(postwoman.environments)
|
||||||
|
)
|
||||||
|
|
||||||
|
delete postwoman.environments
|
||||||
|
window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeColorKey = "THEME_COLOR"
|
||||||
|
let themeColorValue = window.localStorage.getItem(themeColorKey)
|
||||||
|
|
||||||
|
if (themeColorValue) {
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = THEME_COLOR_SCHEMA.safeParse(themeColorValue)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
themeColorValue = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(themeColorKey)
|
||||||
|
window.localStorage.setItem(`${themeColorKey}-backup`, themeColorValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
applySetting(themeColorKey, themeColorValue as HoppAccentColor)
|
||||||
|
window.localStorage.removeItem(themeColorKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nuxtColorModeKey = "nuxt-color-mode"
|
||||||
|
let nuxtColorModeValue = window.localStorage.getItem(nuxtColorModeKey)
|
||||||
|
|
||||||
|
if (nuxtColorModeValue) {
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = NUXT_COLOR_MODE_SCHEMA.safeParse(nuxtColorModeValue)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
nuxtColorModeValue = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(nuxtColorModeKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${nuxtColorModeKey}-backup`,
|
||||||
|
nuxtColorModeValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
applySetting("BG_COLOR", nuxtColorModeValue as HoppBgColor)
|
||||||
|
window.localStorage.removeItem(nuxtColorModeKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setupLocalStatePersistence() {
|
||||||
|
const localStateKey = "localState"
|
||||||
|
let localStateData = JSON.parse(
|
||||||
|
window.localStorage.getItem(localStateKey) ?? "{}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = LOCAL_STATE_SCHEMA.safeParse(localStateData)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
localStateData = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(localStateKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${localStateKey}-backup`,
|
||||||
|
JSON.stringify(localStateData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStateData) bulkApplyLocalState(localStateData)
|
||||||
|
|
||||||
|
localStateStore.subject$.subscribe((state) => {
|
||||||
|
window.localStorage.setItem(localStateKey, JSON.stringify(state))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupSettingsPersistence() {
|
||||||
|
const settingsKey = "settings"
|
||||||
|
let settingsData = JSON.parse(
|
||||||
|
window.localStorage.getItem(settingsKey) || "{}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = SETTINGS_SCHEMA.safeParse(settingsData)
|
||||||
|
if (result.success) {
|
||||||
|
settingsData = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(settingsKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${settingsKey}-backup`,
|
||||||
|
JSON.stringify(settingsData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSettings = settingsData
|
||||||
|
? performSettingsDataMigrations(settingsData)
|
||||||
|
: settingsData
|
||||||
|
|
||||||
|
if (updatedSettings) {
|
||||||
|
bulkApplySettings(updatedSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsStore.subject$.subscribe((settings) => {
|
||||||
|
window.localStorage.setItem(settingsKey, JSON.stringify(settings))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupHistoryPersistence() {
|
||||||
|
const restHistoryKey = "history"
|
||||||
|
let restHistoryData = JSON.parse(
|
||||||
|
window.localStorage.getItem(restHistoryKey) || "[]"
|
||||||
|
)
|
||||||
|
|
||||||
|
const graphqlHistoryKey = "graphqlHistory"
|
||||||
|
let graphqlHistoryData = JSON.parse(
|
||||||
|
window.localStorage.getItem(graphqlHistoryKey) || "[]"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const restHistorySchemaParsedresult = z
|
||||||
|
.array(REST_HISTORY_ENTRY_SCHEMA)
|
||||||
|
.safeParse(restHistoryData)
|
||||||
|
|
||||||
|
if (restHistorySchemaParsedresult.success) {
|
||||||
|
restHistoryData = restHistorySchemaParsedresult.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(restHistoryKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${restHistoryKey}-backup`,
|
||||||
|
JSON.stringify(restHistoryData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const gqlHistorySchemaParsedresult = z
|
||||||
|
.array(GQL_HISTORY_ENTRY_SCHEMA)
|
||||||
|
.safeParse(graphqlHistoryData)
|
||||||
|
|
||||||
|
if (gqlHistorySchemaParsedresult.success) {
|
||||||
|
graphqlHistoryData = gqlHistorySchemaParsedresult.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(graphqlHistoryKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${graphqlHistoryKey}-backup`,
|
||||||
|
JSON.stringify(graphqlHistoryData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const translatedRestHistoryData = restHistoryData.map(
|
||||||
|
translateToNewRESTHistory
|
||||||
|
)
|
||||||
|
const translatedGraphqlHistoryData = graphqlHistoryData.map(
|
||||||
|
translateToNewGQLHistory
|
||||||
|
)
|
||||||
|
|
||||||
|
setRESTHistoryEntries(translatedRestHistoryData)
|
||||||
|
setGraphqlHistoryEntries(translatedGraphqlHistoryData)
|
||||||
|
|
||||||
|
restHistoryStore.subject$.subscribe(({ state }) => {
|
||||||
|
window.localStorage.setItem(restHistoryKey, JSON.stringify(state))
|
||||||
|
})
|
||||||
|
|
||||||
|
graphqlHistoryStore.subject$.subscribe(({ state }) => {
|
||||||
|
window.localStorage.setItem(graphqlHistoryKey, JSON.stringify(state))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupCollectionsPersistence() {
|
||||||
|
const restCollectionsKey = "collections"
|
||||||
|
let restCollectionsData = JSON.parse(
|
||||||
|
window.localStorage.getItem(restCollectionsKey) || "[]"
|
||||||
|
)
|
||||||
|
|
||||||
|
const graphqlCollectionsKey = "collectionsGraphql"
|
||||||
|
let graphqlCollectionsData = JSON.parse(
|
||||||
|
window.localStorage.getItem(graphqlCollectionsKey) || "[]"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const restCollectionsSchemaParsedresult = z
|
||||||
|
.array(REST_COLLECTION_SCHEMA)
|
||||||
|
.safeParse(restCollectionsData)
|
||||||
|
|
||||||
|
if (restCollectionsSchemaParsedresult.success) {
|
||||||
|
restCollectionsData = restCollectionsSchemaParsedresult.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(restCollectionsKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${restCollectionsKey}-backup`,
|
||||||
|
JSON.stringify(restCollectionsData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const gqlCollectionsSchemaParsedresult = z
|
||||||
|
.array(GQL_COLLECTION_SCHEMA)
|
||||||
|
.safeParse(graphqlCollectionsData)
|
||||||
|
|
||||||
|
if (gqlCollectionsSchemaParsedresult.success) {
|
||||||
|
graphqlCollectionsData = gqlCollectionsSchemaParsedresult.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(graphqlCollectionsKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${graphqlCollectionsKey}-backup`,
|
||||||
|
JSON.stringify(graphqlCollectionsData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const translatedRestCollectionsData = restCollectionsData.map(
|
||||||
|
translateToNewRESTCollection
|
||||||
|
)
|
||||||
|
const translatedGraphqlCollectionsData = graphqlCollectionsData.map(
|
||||||
|
translateToNewGQLCollection
|
||||||
|
)
|
||||||
|
|
||||||
|
setRESTCollections(translatedRestCollectionsData)
|
||||||
|
setGraphqlCollections(translatedGraphqlCollectionsData)
|
||||||
|
|
||||||
|
restCollectionStore.subject$.subscribe(({ state }) => {
|
||||||
|
window.localStorage.setItem(restCollectionsKey, JSON.stringify(state))
|
||||||
|
})
|
||||||
|
|
||||||
|
graphqlCollectionStore.subject$.subscribe(({ state }) => {
|
||||||
|
window.localStorage.setItem(graphqlCollectionsKey, JSON.stringify(state))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupEnvironmentsPersistence() {
|
||||||
|
const environmentsKey = "environments"
|
||||||
|
let environmentsData: Environment[] = JSON.parse(
|
||||||
|
window.localStorage.getItem(environmentsKey) || "[]"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = ENVIRONMENTS_SCHEMA.safeParse(environmentsData)
|
||||||
|
if (result.success) {
|
||||||
|
environmentsData = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(environmentsKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${environmentsKey}-backup`,
|
||||||
|
JSON.stringify(environmentsData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a global env is defined and if so move that to globals
|
||||||
|
const globalIndex = environmentsData.findIndex(
|
||||||
|
(x) => x.name.toLowerCase() === "globals"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (globalIndex !== -1) {
|
||||||
|
const globalEnv = environmentsData[globalIndex]
|
||||||
|
globalEnv.variables.forEach((variable) => addGlobalEnvVariable(variable))
|
||||||
|
|
||||||
|
// Remove global from environments
|
||||||
|
environmentsData.splice(globalIndex, 1)
|
||||||
|
|
||||||
|
// Just sync the changes manually
|
||||||
|
window.localStorage.setItem(
|
||||||
|
environmentsKey,
|
||||||
|
JSON.stringify(environmentsData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceEnvironments(environmentsData)
|
||||||
|
|
||||||
|
environments$.subscribe((envs) => {
|
||||||
|
window.localStorage.setItem(environmentsKey, JSON.stringify(envs))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupSelectedEnvPersistence() {
|
||||||
|
const selectedEnvIndexKey = "selectedEnvIndex"
|
||||||
|
let selectedEnvIndexValue = JSON.parse(
|
||||||
|
window.localStorage.getItem(selectedEnvIndexKey) ?? "null"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = SELECTED_ENV_INDEX_SCHEMA.safeParse(selectedEnvIndexValue)
|
||||||
|
if (result.success) {
|
||||||
|
selectedEnvIndexValue = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(selectedEnvIndexKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${selectedEnvIndexKey}-backup`,
|
||||||
|
JSON.stringify(selectedEnvIndexValue)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is a selected env index, set it to the store else set it to null
|
||||||
|
if (selectedEnvIndexValue) {
|
||||||
|
setSelectedEnvironmentIndex(selectedEnvIndexValue)
|
||||||
|
} else {
|
||||||
|
setSelectedEnvironmentIndex({
|
||||||
|
type: "NO_ENV_SELECTED",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedEnvironmentIndex$.subscribe((envIndex) => {
|
||||||
|
window.localStorage.setItem(selectedEnvIndexKey, JSON.stringify(envIndex))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupWebsocketPersistence() {
|
||||||
|
const wsRequestKey = "WebsocketRequest"
|
||||||
|
let wsRequestData = JSON.parse(
|
||||||
|
window.localStorage.getItem(wsRequestKey) || "null"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = WEBSOCKET_REQUEST_SCHEMA.safeParse(wsRequestData)
|
||||||
|
if (result.success) {
|
||||||
|
wsRequestData = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(wsRequestKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${wsRequestKey}-backup`,
|
||||||
|
JSON.stringify(wsRequestData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setWSRequest(wsRequestData)
|
||||||
|
|
||||||
|
WSRequest$.subscribe((req) => {
|
||||||
|
window.localStorage.setItem(wsRequestKey, JSON.stringify(req))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupSocketIOPersistence() {
|
||||||
|
const sioRequestKey = "SocketIORequest"
|
||||||
|
let sioRequestData = JSON.parse(
|
||||||
|
window.localStorage.getItem(sioRequestKey) || "null"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = SOCKET_IO_REQUEST_SCHEMA.safeParse(sioRequestData)
|
||||||
|
if (result.success) {
|
||||||
|
sioRequestData = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(sioRequestKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${sioRequestKey}-backup`,
|
||||||
|
JSON.stringify(sioRequestData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSIORequest(sioRequestData)
|
||||||
|
|
||||||
|
SIORequest$.subscribe((req) => {
|
||||||
|
window.localStorage.setItem(sioRequestKey, JSON.stringify(req))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupSSEPersistence() {
|
||||||
|
const sseRequestKey = "SSERequest"
|
||||||
|
let sseRequestData = JSON.parse(
|
||||||
|
window.localStorage.getItem(sseRequestKey) || "null"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = SSE_REQUEST_SCHEMA.safeParse(sseRequestData)
|
||||||
|
if (result.success) {
|
||||||
|
sseRequestData = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(sseRequestKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${sseRequestKey}-backup`,
|
||||||
|
JSON.stringify(sseRequestData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSSERequest(sseRequestData)
|
||||||
|
|
||||||
|
SSERequest$.subscribe((req) => {
|
||||||
|
window.localStorage.setItem(sseRequestKey, JSON.stringify(req))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupMQTTPersistence() {
|
||||||
|
const mqttRequestKey = "MQTTRequest"
|
||||||
|
let mqttRequestData = JSON.parse(
|
||||||
|
window.localStorage.getItem(mqttRequestKey) || "null"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = MQTT_REQUEST_SCHEMA.safeParse(mqttRequestData)
|
||||||
|
if (result.success) {
|
||||||
|
mqttRequestData = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(mqttRequestKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${mqttRequestKey}-backup`,
|
||||||
|
JSON.stringify(mqttRequestData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setMQTTRequest(mqttRequestData)
|
||||||
|
|
||||||
|
MQTTRequest$.subscribe((req) => {
|
||||||
|
window.localStorage.setItem(mqttRequestKey, JSON.stringify(req))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupGlobalEnvsPersistence() {
|
||||||
|
const globalEnvKey = "globalEnv"
|
||||||
|
let globalEnvData: Environment["variables"] = JSON.parse(
|
||||||
|
window.localStorage.getItem(globalEnvKey) || "[]"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = GLOBAL_ENV_SCHEMA.safeParse(globalEnvData)
|
||||||
|
if (result.success) {
|
||||||
|
globalEnvData = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(globalEnvKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${globalEnvKey}-backup`,
|
||||||
|
JSON.stringify(globalEnvData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setGlobalEnvVariables(globalEnvData)
|
||||||
|
|
||||||
|
globalEnv$.subscribe((vars) => {
|
||||||
|
window.localStorage.setItem(globalEnvKey, JSON.stringify(vars))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupGQLTabsPersistence() {
|
||||||
|
const gqlTabStateKey = "gqlTabState"
|
||||||
|
const gqlTabStateData = window.localStorage.getItem(gqlTabStateKey)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (gqlTabStateData) {
|
||||||
|
let parsedGqlTabStateData = JSON.parse(gqlTabStateData)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = GQL_TAB_STATE_SCHEMA.safeParse(parsedGqlTabStateData)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
parsedGqlTabStateData = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(gqlTabStateKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${gqlTabStateKey}-backup`,
|
||||||
|
JSON.stringify(parsedGqlTabStateData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gqlTabService.loadTabsFromPersistedState(parsedGqlTabStateData)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`Failed parsing persisted tab state, state:`,
|
||||||
|
gqlTabStateData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDebounced(
|
||||||
|
this.gqlTabService.persistableTabState,
|
||||||
|
(newGqlTabStateData) => {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
gqlTabStateKey,
|
||||||
|
JSON.stringify(newGqlTabStateData)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ debounce: 500, deep: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupRESTTabsPersistence() {
|
||||||
|
const restTabStateKey = "restTabState"
|
||||||
|
const restTabStateData = window.localStorage.getItem(restTabStateKey)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (restTabStateData) {
|
||||||
|
let parsedGqlTabStateData = JSON.parse(restTabStateData)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = REST_TAB_STATE_SCHEMA.safeParse(parsedGqlTabStateData)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
parsedGqlTabStateData = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(restTabStateKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${restTabStateKey}-backup`,
|
||||||
|
JSON.stringify(parsedGqlTabStateData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.restTabService.loadTabsFromPersistedState(parsedGqlTabStateData)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`Failed parsing persisted tab state, state:`,
|
||||||
|
restTabStateData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDebounced(
|
||||||
|
this.restTabService.persistableTabState,
|
||||||
|
(newRestTabStateData) => {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
restTabStateKey,
|
||||||
|
JSON.stringify(newRestTabStateData)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ debounce: 500, deep: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public setupLocalPersistence() {
|
||||||
|
this.checkAndMigrateOldSettings()
|
||||||
|
|
||||||
|
this.setupLocalStatePersistence()
|
||||||
|
this.setupSettingsPersistence()
|
||||||
|
this.setupRESTTabsPersistence()
|
||||||
|
|
||||||
|
this.setupGQLTabsPersistence()
|
||||||
|
|
||||||
|
this.setupHistoryPersistence()
|
||||||
|
this.setupCollectionsPersistence()
|
||||||
|
this.setupGlobalEnvsPersistence()
|
||||||
|
this.setupEnvironmentsPersistence()
|
||||||
|
this.setupSelectedEnvPersistence()
|
||||||
|
this.setupWebsocketPersistence()
|
||||||
|
this.setupSocketIOPersistence()
|
||||||
|
this.setupSSEPersistence()
|
||||||
|
this.setupMQTTPersistence()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a value from localStorage
|
||||||
|
*
|
||||||
|
* NOTE: Use localStorage to only store non-reactive simple data
|
||||||
|
* For more complex data, use stores and connect it to `PersistenceService`
|
||||||
|
*/
|
||||||
|
public getLocalConfig(name: string) {
|
||||||
|
return window.localStorage.getItem(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a value in localStorage
|
||||||
|
*
|
||||||
|
* NOTE: Use localStorage to only store non-reactive simple data
|
||||||
|
* For more complex data, use stores and connect it to `PersistenceService`
|
||||||
|
*/
|
||||||
|
public setLocalConfig(key: string, value: string) {
|
||||||
|
window.localStorage.setItem(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear config value in localStorage
|
||||||
|
*/
|
||||||
|
public removeLocalConfig(key: string) {
|
||||||
|
window.localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,470 @@
|
|||||||
|
import {
|
||||||
|
Environment,
|
||||||
|
GQLHeader,
|
||||||
|
HoppGQLAuth,
|
||||||
|
HoppGQLRequest,
|
||||||
|
HoppRESTRequest,
|
||||||
|
} from "@hoppscotch/data"
|
||||||
|
import { entityReference } from "verzod"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { HoppAccentColors, HoppBgColors } from "~/newstore/settings"
|
||||||
|
|
||||||
|
const ThemeColorSchema = z.enum([
|
||||||
|
"green",
|
||||||
|
"teal",
|
||||||
|
"blue",
|
||||||
|
"indigo",
|
||||||
|
"purple",
|
||||||
|
"yellow",
|
||||||
|
"orange",
|
||||||
|
"red",
|
||||||
|
"pink",
|
||||||
|
])
|
||||||
|
|
||||||
|
const BgColorSchema = z.enum(["system", "light", "dark", "black"])
|
||||||
|
|
||||||
|
const SettingsDefSchema = z.object({
|
||||||
|
syncCollections: z.boolean(),
|
||||||
|
syncHistory: z.boolean(),
|
||||||
|
syncEnvironments: z.boolean(),
|
||||||
|
PROXY_URL: z.string(),
|
||||||
|
CURRENT_INTERCEPTOR_ID: z.string(),
|
||||||
|
URL_EXCLUDES: z.object({
|
||||||
|
auth: z.boolean(),
|
||||||
|
httpUser: z.boolean(),
|
||||||
|
httpPassword: z.boolean(),
|
||||||
|
bearerToken: z.boolean(),
|
||||||
|
oauth2Token: z.boolean(),
|
||||||
|
}),
|
||||||
|
THEME_COLOR: ThemeColorSchema,
|
||||||
|
BG_COLOR: BgColorSchema,
|
||||||
|
TELEMETRY_ENABLED: z.boolean(),
|
||||||
|
EXPAND_NAVIGATION: z.boolean(),
|
||||||
|
SIDEBAR: z.boolean(),
|
||||||
|
SIDEBAR_ON_LEFT: z.boolean(),
|
||||||
|
COLUMN_LAYOUT: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Common properties shared across REST & GQL collections
|
||||||
|
const HoppCollectionSchemaCommonProps = z
|
||||||
|
.object({
|
||||||
|
v: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
id: z.optional(z.string()),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
const HoppRESTRequestSchema = entityReference(HoppRESTRequest)
|
||||||
|
|
||||||
|
const HoppGQLRequestSchema = entityReference(HoppGQLRequest)
|
||||||
|
|
||||||
|
// @ts-expect-error recursive schema
|
||||||
|
const HoppRESTCollectionSchema = HoppCollectionSchemaCommonProps.extend({
|
||||||
|
folders: z.array(z.lazy(() => HoppRESTCollectionSchema)),
|
||||||
|
requests: z.optional(z.array(HoppRESTRequestSchema)),
|
||||||
|
}).strict()
|
||||||
|
|
||||||
|
// @ts-expect-error recursive schema
|
||||||
|
const HoppGQLCollectionSchema = HoppCollectionSchemaCommonProps.extend({
|
||||||
|
folders: z.array(z.lazy(() => HoppGQLCollectionSchema)),
|
||||||
|
requests: z.optional(z.array(HoppGQLRequestSchema)),
|
||||||
|
}).strict()
|
||||||
|
|
||||||
|
export const VUEX_SCHEMA = z.object({
|
||||||
|
postwoman: z.optional(
|
||||||
|
z.object({
|
||||||
|
settings: z.optional(SettingsDefSchema),
|
||||||
|
//! Versioned entities
|
||||||
|
collections: z.optional(z.array(HoppRESTCollectionSchema)),
|
||||||
|
collectionsGraphql: z.optional(z.array(HoppGQLCollectionSchema)),
|
||||||
|
environments: z.optional(z.array(entityReference(Environment))),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const THEME_COLOR_SCHEMA = z.enum(HoppAccentColors)
|
||||||
|
|
||||||
|
export const NUXT_COLOR_MODE_SCHEMA = z.enum(HoppBgColors)
|
||||||
|
|
||||||
|
export const LOCAL_STATE_SCHEMA = z.union([
|
||||||
|
z.object({}).strict(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
REMEMBERED_TEAM_ID: z.optional(z.string()),
|
||||||
|
})
|
||||||
|
.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 REST_HISTORY_ENTRY_SCHEMA = z
|
||||||
|
.object({
|
||||||
|
v: z.number(),
|
||||||
|
//! Versioned entity
|
||||||
|
request: HoppRESTRequestSchema,
|
||||||
|
responseMeta: z
|
||||||
|
.object({
|
||||||
|
duration: z.nullable(z.number()),
|
||||||
|
statusCode: z.nullable(z.number()),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
star: z.boolean(),
|
||||||
|
id: z.optional(z.string()),
|
||||||
|
updatedOn: z.optional(z.union([z.date(), z.string()])),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
export const GQL_HISTORY_ENTRY_SCHEMA = z
|
||||||
|
.object({
|
||||||
|
v: z.number(),
|
||||||
|
//! Versioned entity
|
||||||
|
request: HoppGQLRequestSchema,
|
||||||
|
response: z.string(),
|
||||||
|
star: z.boolean(),
|
||||||
|
id: z.optional(z.string()),
|
||||||
|
updatedOn: z.optional(z.union([z.date(), z.string()])),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
export const REST_COLLECTION_SCHEMA = HoppRESTCollectionSchema
|
||||||
|
|
||||||
|
export const GQL_COLLECTION_SCHEMA = HoppGQLCollectionSchema
|
||||||
|
|
||||||
|
export const ENVIRONMENTS_SCHEMA = z.array(entityReference(Environment))
|
||||||
|
|
||||||
|
export const SELECTED_ENV_INDEX_SCHEMA = z.nullable(
|
||||||
|
z.discriminatedUnion("type", [
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("NO_ENV_SELECTED"),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("MY_ENV"),
|
||||||
|
index: z.number(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
z.object({
|
||||||
|
type: z.literal("TEAM_ENV"),
|
||||||
|
teamID: z.string(),
|
||||||
|
teamEnvID: z.string(),
|
||||||
|
// ! Versioned entity
|
||||||
|
environment: entityReference(Environment),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
export const WEBSOCKET_REQUEST_SCHEMA = z.nullable(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
endpoint: z.string(),
|
||||||
|
protocols: z.array(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
value: z.string(),
|
||||||
|
active: z.boolean(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
)
|
||||||
|
|
||||||
|
export const SOCKET_IO_REQUEST_SCHEMA = z.nullable(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
endpoint: z.string(),
|
||||||
|
path: z.string(),
|
||||||
|
version: z.union([z.literal("v4"), z.literal("v3"), z.literal("v2")]),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
)
|
||||||
|
|
||||||
|
export const SSE_REQUEST_SCHEMA = z.nullable(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
endpoint: z.string(),
|
||||||
|
eventType: z.string(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
)
|
||||||
|
|
||||||
|
export const MQTT_REQUEST_SCHEMA = z.nullable(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
endpoint: z.string(),
|
||||||
|
clientID: z.string(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
)
|
||||||
|
|
||||||
|
export const GLOBAL_ENV_SCHEMA = z.union([
|
||||||
|
z.array(z.never()),
|
||||||
|
z.array(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
const OperationTypeSchema = z.enum([
|
||||||
|
"subscription",
|
||||||
|
"query",
|
||||||
|
"mutation",
|
||||||
|
"teardown",
|
||||||
|
])
|
||||||
|
|
||||||
|
const RunQueryOptionsSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.optional(z.string()),
|
||||||
|
url: z.string(),
|
||||||
|
headers: z.array(GQLHeader),
|
||||||
|
query: z.string(),
|
||||||
|
variables: z.string(),
|
||||||
|
auth: HoppGQLAuth,
|
||||||
|
operationName: z.optional(z.string()),
|
||||||
|
operationType: OperationTypeSchema,
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
const HoppGQLSaveContextSchema = z.nullable(
|
||||||
|
z.discriminatedUnion("originLocation", [
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
originLocation: z.literal("user-collection"),
|
||||||
|
folderPath: z.string(),
|
||||||
|
requestIndex: z.number(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
originLocation: z.literal("team-collection"),
|
||||||
|
requestID: z.string(),
|
||||||
|
teamID: z.optional(z.string()),
|
||||||
|
collectionID: z.optional(z.string()),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const GQLResponseEventSchema = z.array(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
time: z.number(),
|
||||||
|
operationName: z.optional(z.string()),
|
||||||
|
operationType: OperationTypeSchema,
|
||||||
|
data: z.string(),
|
||||||
|
rawQuery: z.optional(RunQueryOptionsSchema),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
)
|
||||||
|
|
||||||
|
const validGqlOperations = [
|
||||||
|
"query",
|
||||||
|
"headers",
|
||||||
|
"variables",
|
||||||
|
"authorization",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const GQL_TAB_STATE_SCHEMA = z
|
||||||
|
.object({
|
||||||
|
lastActiveTabID: z.string(),
|
||||||
|
orderedDocs: z.array(
|
||||||
|
z.object({
|
||||||
|
tabID: z.string(),
|
||||||
|
doc: z
|
||||||
|
.object({
|
||||||
|
// Versioned entity
|
||||||
|
request: entityReference(HoppGQLRequest),
|
||||||
|
isDirty: z.boolean(),
|
||||||
|
saveContext: z.optional(HoppGQLSaveContextSchema),
|
||||||
|
response: z.optional(z.nullable(GQLResponseEventSchema)),
|
||||||
|
responseTabPreference: z.optional(z.string()),
|
||||||
|
optionTabPreference: z.optional(z.enum(validGqlOperations)),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
const HoppTestExpectResultSchema = z
|
||||||
|
.object({
|
||||||
|
status: z.enum(["fail", "pass", "error"]),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
// @ts-expect-error recursive schema
|
||||||
|
const HoppTestDataSchema = z.lazy(() =>
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
description: z.string(),
|
||||||
|
expectResults: z.array(HoppTestExpectResultSchema),
|
||||||
|
tests: z.array(HoppTestDataSchema),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
)
|
||||||
|
|
||||||
|
const EnvironmentVariablesSchema = z
|
||||||
|
.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
const HoppTestResultSchema = z
|
||||||
|
.object({
|
||||||
|
tests: z.array(HoppTestDataSchema),
|
||||||
|
expectResults: z.array(HoppTestExpectResultSchema),
|
||||||
|
description: z.string(),
|
||||||
|
scriptError: z.boolean(),
|
||||||
|
envDiff: z
|
||||||
|
.object({
|
||||||
|
global: z
|
||||||
|
.object({
|
||||||
|
additions: z.array(EnvironmentVariablesSchema),
|
||||||
|
updations: z.array(
|
||||||
|
EnvironmentVariablesSchema.extend({ previousValue: z.string() })
|
||||||
|
),
|
||||||
|
deletions: z.array(EnvironmentVariablesSchema),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
selected: z
|
||||||
|
.object({
|
||||||
|
additions: z.array(EnvironmentVariablesSchema),
|
||||||
|
updations: z.array(
|
||||||
|
EnvironmentVariablesSchema.extend({ previousValue: z.string() })
|
||||||
|
),
|
||||||
|
deletions: z.array(EnvironmentVariablesSchema),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
const HoppRESTResponseHeaderSchema = z
|
||||||
|
.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
const HoppRESTResponseSchema = z.discriminatedUnion("type", [
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("loading"),
|
||||||
|
// !Versioned entity
|
||||||
|
req: HoppRESTRequestSchema,
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("fail"),
|
||||||
|
headers: z.array(HoppRESTResponseHeaderSchema),
|
||||||
|
body: z.instanceof(ArrayBuffer),
|
||||||
|
statusCode: z.number(),
|
||||||
|
meta: z
|
||||||
|
.object({
|
||||||
|
responseSize: z.number(),
|
||||||
|
responseDuration: z.number(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
// !Versioned entity
|
||||||
|
req: HoppRESTRequestSchema,
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("network_fail"),
|
||||||
|
error: z.unknown(),
|
||||||
|
// !Versioned entity
|
||||||
|
req: HoppRESTRequestSchema,
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("script_fail"),
|
||||||
|
error: z.instanceof(Error),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("success"),
|
||||||
|
headers: z.array(HoppRESTResponseHeaderSchema),
|
||||||
|
body: z.instanceof(ArrayBuffer),
|
||||||
|
statusCode: z.number(),
|
||||||
|
meta: z
|
||||||
|
.object({
|
||||||
|
responseSize: z.number(),
|
||||||
|
responseDuration: z.number(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
// !Versioned entity
|
||||||
|
req: HoppRESTRequestSchema,
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
])
|
||||||
|
|
||||||
|
const HoppRESTSaveContextSchema = z.nullable(
|
||||||
|
z.discriminatedUnion("originLocation", [
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
originLocation: z.literal("user-collection"),
|
||||||
|
folderPath: z.string(),
|
||||||
|
requestIndex: z.number(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
originLocation: z.literal("team-collection"),
|
||||||
|
requestID: z.string(),
|
||||||
|
teamID: z.optional(z.string()),
|
||||||
|
collectionID: z.optional(z.string()),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const validRestOperations = [
|
||||||
|
"params",
|
||||||
|
"bodyParams",
|
||||||
|
"headers",
|
||||||
|
"authorization",
|
||||||
|
"preRequestScript",
|
||||||
|
"tests",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const REST_TAB_STATE_SCHEMA = z
|
||||||
|
.object({
|
||||||
|
lastActiveTabID: z.string(),
|
||||||
|
orderedDocs: z.array(
|
||||||
|
z.object({
|
||||||
|
tabID: z.string(),
|
||||||
|
doc: z
|
||||||
|
.object({
|
||||||
|
// !Versioned entity
|
||||||
|
request: entityReference(HoppRESTRequest),
|
||||||
|
isDirty: z.boolean(),
|
||||||
|
saveContext: z.optional(HoppRESTSaveContextSchema),
|
||||||
|
response: z.optional(z.nullable(HoppRESTResponseSchema)),
|
||||||
|
testResults: z.optional(z.nullable(HoppTestResultSchema)),
|
||||||
|
responseTabPreference: z.optional(z.string()),
|
||||||
|
optionTabPreference: z.optional(z.enum(validRestOperations)),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
import axios from "axios"
|
import { getService } from "@hoppscotch/common/modules/dioc"
|
||||||
import {
|
import {
|
||||||
AuthEvent,
|
AuthEvent,
|
||||||
AuthPlatformDef,
|
AuthPlatformDef,
|
||||||
HoppUser,
|
HoppUser,
|
||||||
} from "@hoppscotch/common/platform/auth"
|
} from "@hoppscotch/common/platform/auth"
|
||||||
import { BehaviorSubject, Subject } from "rxjs"
|
|
||||||
import {
|
import {
|
||||||
getLocalConfig,
|
PersistenceService
|
||||||
removeLocalConfig,
|
} from "@hoppscotch/common/services/persistence"
|
||||||
setLocalConfig,
|
|
||||||
} from "@hoppscotch/common/newstore/localpersistence"
|
|
||||||
import { Ref, ref, watch } from "vue"
|
|
||||||
import { open } from '@tauri-apps/api/shell'
|
|
||||||
import { Body, getClient } from '@tauri-apps/api/http'
|
|
||||||
import { listen } from '@tauri-apps/api/event'
|
import { listen } from '@tauri-apps/api/event'
|
||||||
import { Store } from "tauri-plugin-store-api";
|
import { Body, getClient } from '@tauri-apps/api/http'
|
||||||
|
import { open } from '@tauri-apps/api/shell'
|
||||||
|
import { BehaviorSubject, Subject } from "rxjs"
|
||||||
|
import { Store } from "tauri-plugin-store-api"
|
||||||
|
import { Ref, ref, watch } from "vue"
|
||||||
|
|
||||||
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
|
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
|
||||||
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||||
@@ -22,6 +20,8 @@ export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
|
|||||||
|
|
||||||
const APP_DATA_PATH = "~/.hopp-desktop-app-data.dat"
|
const APP_DATA_PATH = "~/.hopp-desktop-app-data.dat"
|
||||||
|
|
||||||
|
const persistenceService = getService(PersistenceService)
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
let client = await getClient();
|
let client = await getClient();
|
||||||
await client.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/logout`)
|
await client.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/logout`)
|
||||||
@@ -86,7 +86,7 @@ function setUser(user: HoppUser | null) {
|
|||||||
currentUser$.next(user)
|
currentUser$.next(user)
|
||||||
probableUser$.next(user)
|
probableUser$.next(user)
|
||||||
|
|
||||||
setLocalConfig("login_state", JSON.stringify(user))
|
persistenceService.setLocalConfig("login_state", JSON.stringify(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setInitialUser() {
|
async function setInitialUser() {
|
||||||
@@ -181,7 +181,7 @@ async function sendMagicLink(email: string) {
|
|||||||
const res = await client.post(url, Body.json({ email }));
|
const res = await client.post(url, Body.json({ email }));
|
||||||
|
|
||||||
if (res.data && res.data.deviceIdentifier) {
|
if (res.data && res.data.deviceIdentifier) {
|
||||||
setLocalConfig("deviceIdentifier", res.data.deviceIdentifier)
|
persistenceService.setLocalConfig("deviceIdentifier", res.data.deviceIdentifier)
|
||||||
} else {
|
} else {
|
||||||
throw new Error("test: does not get device identifier")
|
throw new Error("test: does not get device identifier")
|
||||||
}
|
}
|
||||||
@@ -257,7 +257,7 @@ export const def: AuthPlatformDef = {
|
|||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
async performAuthInit() {
|
async performAuthInit() {
|
||||||
const probableUser = JSON.parse(getLocalConfig("login_state") ?? "null")
|
const probableUser = JSON.parse(persistenceService.getLocalConfig("login_state") ?? "null")
|
||||||
probableUser$.next(probableUser)
|
probableUser$.next(probableUser)
|
||||||
await setInitialUser()
|
await setInitialUser()
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ export const def: AuthPlatformDef = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isNotNullOrUndefined(token)) {
|
if (isNotNullOrUndefined(token)) {
|
||||||
setLocalConfig("verifyToken", token)
|
persistenceService.setLocalConfig("verifyToken", token)
|
||||||
await this.signInWithEmailLink("", "")
|
await this.signInWithEmailLink("", "")
|
||||||
await setInitialUser()
|
await setInitialUser()
|
||||||
}
|
}
|
||||||
@@ -327,7 +327,7 @@ export const def: AuthPlatformDef = {
|
|||||||
await signInUserWithMicrosoftFB()
|
await signInUserWithMicrosoftFB()
|
||||||
},
|
},
|
||||||
async signInWithEmailLink(_email, _url) {
|
async signInWithEmailLink(_email, _url) {
|
||||||
const deviceIdentifier = getLocalConfig("deviceIdentifier")
|
const deviceIdentifier = persistenceService.getLocalConfig("deviceIdentifier")
|
||||||
|
|
||||||
if (!deviceIdentifier) {
|
if (!deviceIdentifier) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -335,7 +335,7 @@ export const def: AuthPlatformDef = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let verifyToken = getLocalConfig("verifyToken")
|
let verifyToken = persistenceService.getLocalConfig("verifyToken")
|
||||||
|
|
||||||
const client = await getClient();
|
const client = await getClient();
|
||||||
let res = await client.post(`${import.meta.env.VITE_BACKEND_API_URL}/auth/verify`, Body.json({
|
let res = await client.post(`${import.meta.env.VITE_BACKEND_API_URL}/auth/verify`, Body.json({
|
||||||
@@ -345,8 +345,8 @@ export const def: AuthPlatformDef = {
|
|||||||
|
|
||||||
setAuthCookies(res.rawHeaders)
|
setAuthCookies(res.rawHeaders)
|
||||||
|
|
||||||
removeLocalConfig("deviceIdentifier")
|
persistenceService.removeLocalConfig("deviceIdentifier")
|
||||||
removeLocalConfig("verifyToken")
|
persistenceService.removeLocalConfig("verifyToken")
|
||||||
window.location.href = "/"
|
window.location.href = "/"
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
@@ -365,7 +365,7 @@ export const def: AuthPlatformDef = {
|
|||||||
|
|
||||||
probableUser$.next(null)
|
probableUser$.next(null)
|
||||||
currentUser$.next(null)
|
currentUser$.next(null)
|
||||||
removeLocalConfig("login_state")
|
persistenceService.removeLocalConfig("login_state")
|
||||||
|
|
||||||
authEvents$.next({
|
authEvents$.next({
|
||||||
event: "logout",
|
event: "logout",
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import axios from "axios"
|
import { getService } from "@hoppscotch/common/modules/dioc"
|
||||||
import {
|
import {
|
||||||
AuthEvent,
|
AuthEvent,
|
||||||
AuthPlatformDef,
|
AuthPlatformDef,
|
||||||
HoppUser,
|
HoppUser,
|
||||||
} from "@hoppscotch/common/platform/auth"
|
} from "@hoppscotch/common/platform/auth"
|
||||||
|
import { PersistenceService } from "@hoppscotch/common/services/persistence"
|
||||||
|
import axios from "axios"
|
||||||
import { BehaviorSubject, Subject } from "rxjs"
|
import { BehaviorSubject, Subject } from "rxjs"
|
||||||
import {
|
|
||||||
getLocalConfig,
|
|
||||||
removeLocalConfig,
|
|
||||||
setLocalConfig,
|
|
||||||
} from "@hoppscotch/common/newstore/localpersistence"
|
|
||||||
import { Ref, ref, watch } from "vue"
|
import { Ref, ref, watch } from "vue"
|
||||||
|
|
||||||
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
|
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
|
||||||
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||||
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
|
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||||
|
|
||||||
|
const persistenceService = getService(PersistenceService)
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await axios.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/logout`, {
|
await axios.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/logout`, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
@@ -83,7 +82,7 @@ function setUser(user: HoppUser | null) {
|
|||||||
currentUser$.next(user)
|
currentUser$.next(user)
|
||||||
probableUser$.next(user)
|
probableUser$.next(user)
|
||||||
|
|
||||||
setLocalConfig("login_state", JSON.stringify(user))
|
persistenceService.setLocalConfig("login_state", JSON.stringify(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setInitialUser() {
|
async function setInitialUser() {
|
||||||
@@ -176,7 +175,10 @@ async function sendMagicLink(email: string) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (res.data && res.data.deviceIdentifier) {
|
if (res.data && res.data.deviceIdentifier) {
|
||||||
setLocalConfig("deviceIdentifier", res.data.deviceIdentifier)
|
persistenceService.setLocalConfig(
|
||||||
|
"deviceIdentifier",
|
||||||
|
res.data.deviceIdentifier
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
throw new Error("test: does not get device identifier")
|
throw new Error("test: does not get device identifier")
|
||||||
}
|
}
|
||||||
@@ -230,7 +232,9 @@ export const def: AuthPlatformDef = {
|
|||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
async performAuthInit() {
|
async performAuthInit() {
|
||||||
const probableUser = JSON.parse(getLocalConfig("login_state") ?? "null")
|
const probableUser = JSON.parse(
|
||||||
|
persistenceService.getLocalConfig("login_state") ?? "null"
|
||||||
|
)
|
||||||
probableUser$.next(probableUser)
|
probableUser$.next(probableUser)
|
||||||
await setInitialUser()
|
await setInitialUser()
|
||||||
},
|
},
|
||||||
@@ -281,7 +285,9 @@ export const def: AuthPlatformDef = {
|
|||||||
const searchParams = new URLSearchParams(urlObject.search)
|
const searchParams = new URLSearchParams(urlObject.search)
|
||||||
|
|
||||||
const token = searchParams.get("token")
|
const token = searchParams.get("token")
|
||||||
const deviceIdentifier = getLocalConfig("deviceIdentifier")
|
|
||||||
|
const deviceIdentifier =
|
||||||
|
persistenceService.getLocalConfig("deviceIdentifier")
|
||||||
|
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`${import.meta.env.VITE_BACKEND_API_URL}/auth/verify`,
|
`${import.meta.env.VITE_BACKEND_API_URL}/auth/verify`,
|
||||||
@@ -310,7 +316,8 @@ export const def: AuthPlatformDef = {
|
|||||||
|
|
||||||
probableUser$.next(null)
|
probableUser$.next(null)
|
||||||
currentUser$.next(null)
|
currentUser$.next(null)
|
||||||
removeLocalConfig("login_state")
|
|
||||||
|
persistenceService.removeLocalConfig("login_state")
|
||||||
|
|
||||||
authEvents$.next({
|
authEvents$.next({
|
||||||
event: "logout",
|
event: "logout",
|
||||||
@@ -319,7 +326,8 @@ export const def: AuthPlatformDef = {
|
|||||||
|
|
||||||
async processMagicLink() {
|
async processMagicLink() {
|
||||||
if (this.isSignInWithEmailLink(window.location.href)) {
|
if (this.isSignInWithEmailLink(window.location.href)) {
|
||||||
const deviceIdentifier = getLocalConfig("deviceIdentifier")
|
const deviceIdentifier =
|
||||||
|
persistenceService.getLocalConfig("deviceIdentifier")
|
||||||
|
|
||||||
if (!deviceIdentifier) {
|
if (!deviceIdentifier) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -329,7 +337,7 @@ export const def: AuthPlatformDef = {
|
|||||||
|
|
||||||
await this.signInWithEmailLink(deviceIdentifier, window.location.href)
|
await this.signInWithEmailLink(deviceIdentifier, window.location.href)
|
||||||
|
|
||||||
removeLocalConfig("deviceIdentifier")
|
persistenceService.removeLocalConfig("deviceIdentifier")
|
||||||
window.location.href = "/"
|
window.location.href = "/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
100
pnpm-lock.yaml
generated
100
pnpm-lock.yaml
generated
@@ -580,6 +580,9 @@ importers:
|
|||||||
uuid:
|
uuid:
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.0.0
|
version: 9.0.0
|
||||||
|
verzod:
|
||||||
|
specifier: ^0.2.0
|
||||||
|
version: 0.2.2(zod@3.22.4)
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.3.4
|
specifier: ^3.3.4
|
||||||
version: 3.3.4
|
version: 3.3.4
|
||||||
@@ -1229,7 +1232,7 @@ importers:
|
|||||||
version: 0.8.0(vite@4.4.9)(vue-router@4.2.5)(vue@3.3.4)
|
version: 0.8.0(vite@4.4.9)(vue-router@4.2.5)(vue@3.3.4)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^0.34.2
|
specifier: ^0.34.2
|
||||||
version: 0.34.2(terser@5.24.0)
|
version: 0.34.2(sass@1.66.0)(terser@5.24.0)
|
||||||
vue-tsc:
|
vue-tsc:
|
||||||
specifier: ^1.8.8
|
specifier: ^1.8.8
|
||||||
version: 1.8.8(typescript@5.1.6)
|
version: 1.8.8(typescript@5.1.6)
|
||||||
@@ -20667,7 +20670,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-9xZPKVYp9DxnM3sd1yAsh/d59iIaswDkai8oTxbursfKYbg/ibjX0IzFt35+VZ8iEW453TVTXztnRvYUQlAfUQ==}
|
resolution: {integrity: sha512-9xZPKVYp9DxnM3sd1yAsh/d59iIaswDkai8oTxbursfKYbg/ibjX0IzFt35+VZ8iEW453TVTXztnRvYUQlAfUQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.10.0
|
acorn: 8.11.2
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
espree: 9.6.1
|
espree: 9.6.1
|
||||||
semver: 7.5.4
|
semver: 7.5.4
|
||||||
@@ -21882,7 +21885,7 @@ packages:
|
|||||||
/mlly@1.4.2:
|
/mlly@1.4.2:
|
||||||
resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==}
|
resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.10.0
|
acorn: 8.11.2
|
||||||
pathe: 1.1.1
|
pathe: 1.1.1
|
||||||
pkg-types: 1.0.3
|
pkg-types: 1.0.3
|
||||||
ufo: 1.3.1
|
ufo: 1.3.1
|
||||||
@@ -26318,7 +26321,7 @@ packages:
|
|||||||
local-pkg: 0.4.3
|
local-pkg: 0.4.3
|
||||||
magic-string: 0.26.7
|
magic-string: 0.26.7
|
||||||
minimatch: 5.1.6
|
minimatch: 5.1.6
|
||||||
resolve: 1.22.4
|
resolve: 1.22.8
|
||||||
unplugin: 0.7.1(vite@3.2.4)
|
unplugin: 0.7.1(vite@3.2.4)
|
||||||
vue: 3.2.45
|
vue: 3.2.45
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -26456,7 +26459,7 @@ packages:
|
|||||||
webpack:
|
webpack:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.10.0
|
acorn: 8.11.2
|
||||||
chokidar: 3.5.3
|
chokidar: 3.5.3
|
||||||
vite: 3.2.4(@types/node@18.18.8)(sass@1.58.0)
|
vite: 3.2.4(@types/node@18.18.8)(sass@1.58.0)
|
||||||
webpack-sources: 3.2.3
|
webpack-sources: 3.2.3
|
||||||
@@ -26782,28 +26785,6 @@ packages:
|
|||||||
- terser
|
- terser
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vite-node@0.34.2(@types/node@18.18.8)(terser@5.24.0):
|
|
||||||
resolution: {integrity: sha512-JtW249Zm3FB+F7pQfH56uWSdlltCo1IOkZW5oHBzeQo0iX4jtC7o1t9aILMGd9kVekXBP2lfJBEQt9rBh07ebA==}
|
|
||||||
engines: {node: '>=v14.18.0'}
|
|
||||||
hasBin: true
|
|
||||||
dependencies:
|
|
||||||
cac: 6.7.14
|
|
||||||
debug: 4.3.4(supports-color@9.2.2)
|
|
||||||
mlly: 1.4.0
|
|
||||||
pathe: 1.1.1
|
|
||||||
picocolors: 1.0.0
|
|
||||||
vite: 4.5.0(@types/node@18.18.8)(terser@5.24.0)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@types/node'
|
|
||||||
- less
|
|
||||||
- lightningcss
|
|
||||||
- sass
|
|
||||||
- stylus
|
|
||||||
- sugarss
|
|
||||||
- supports-color
|
|
||||||
- terser
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/vite-plugin-checker@0.5.1(eslint@8.29.0)(typescript@4.9.3)(vite@3.2.4):
|
/vite-plugin-checker@0.5.1(eslint@8.29.0)(typescript@4.9.3)(vite@3.2.4):
|
||||||
resolution: {integrity: sha512-NFiO1PyK9yGuaeSnJ7Whw9fnxLc1AlELnZoyFURnauBYhbIkx9n+PmIXxSFUuC9iFyACtbJQUAEuQi6yHs2Adg==}
|
resolution: {integrity: sha512-NFiO1PyK9yGuaeSnJ7Whw9fnxLc1AlELnZoyFURnauBYhbIkx9n+PmIXxSFUuC9iFyACtbJQUAEuQi6yHs2Adg==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
@@ -27797,71 +27778,6 @@ packages:
|
|||||||
- terser
|
- terser
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vitest@0.34.2(terser@5.24.0):
|
|
||||||
resolution: {integrity: sha512-WgaIvBbjsSYMq/oiMlXUI7KflELmzM43BEvkdC/8b5CAod4ryAiY2z8uR6Crbi5Pjnu5oOmhKa9sy7uk6paBxQ==}
|
|
||||||
engines: {node: '>=v14.18.0'}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
'@edge-runtime/vm': '*'
|
|
||||||
'@vitest/browser': '*'
|
|
||||||
'@vitest/ui': '*'
|
|
||||||
happy-dom: '*'
|
|
||||||
jsdom: '*'
|
|
||||||
playwright: '*'
|
|
||||||
safaridriver: '*'
|
|
||||||
webdriverio: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@edge-runtime/vm':
|
|
||||||
optional: true
|
|
||||||
'@vitest/browser':
|
|
||||||
optional: true
|
|
||||||
'@vitest/ui':
|
|
||||||
optional: true
|
|
||||||
happy-dom:
|
|
||||||
optional: true
|
|
||||||
jsdom:
|
|
||||||
optional: true
|
|
||||||
playwright:
|
|
||||||
optional: true
|
|
||||||
safaridriver:
|
|
||||||
optional: true
|
|
||||||
webdriverio:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
'@types/chai': 4.3.5
|
|
||||||
'@types/chai-subset': 1.3.3
|
|
||||||
'@types/node': 18.18.8
|
|
||||||
'@vitest/expect': 0.34.2
|
|
||||||
'@vitest/runner': 0.34.2
|
|
||||||
'@vitest/snapshot': 0.34.2
|
|
||||||
'@vitest/spy': 0.34.2
|
|
||||||
'@vitest/utils': 0.34.2
|
|
||||||
acorn: 8.10.0
|
|
||||||
acorn-walk: 8.2.0
|
|
||||||
cac: 6.7.14
|
|
||||||
chai: 4.3.7
|
|
||||||
debug: 4.3.4(supports-color@9.2.2)
|
|
||||||
local-pkg: 0.4.3
|
|
||||||
magic-string: 0.30.2
|
|
||||||
pathe: 1.1.1
|
|
||||||
picocolors: 1.0.0
|
|
||||||
std-env: 3.4.0
|
|
||||||
strip-literal: 1.3.0
|
|
||||||
tinybench: 2.5.0
|
|
||||||
tinypool: 0.7.0
|
|
||||||
vite: 4.5.0(@types/node@18.18.8)(terser@5.24.0)
|
|
||||||
vite-node: 0.34.2(@types/node@18.18.8)(terser@5.24.0)
|
|
||||||
why-is-node-running: 2.2.2
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- less
|
|
||||||
- lightningcss
|
|
||||||
- sass
|
|
||||||
- stylus
|
|
||||||
- sugarss
|
|
||||||
- supports-color
|
|
||||||
- terser
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/vm2@3.9.14:
|
/vm2@3.9.14:
|
||||||
resolution: {integrity: sha512-HgvPHYHeQy8+QhzlFryvSteA4uQLBCOub02mgqdR+0bN/akRZ48TGB1v0aCv7ksyc0HXx16AZtMHKS38alc6TA==}
|
resolution: {integrity: sha512-HgvPHYHeQy8+QhzlFryvSteA4uQLBCOub02mgqdR+0bN/akRZ48TGB1v0aCv7ksyc0HXx16AZtMHKS38alc6TA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
|
|||||||
Reference in New Issue
Block a user