Compare commits
4 Commits
fix/shared
...
revert/aut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20c8973f5d | ||
|
|
461d67ce90 | ||
|
|
492c3a0902 | ||
|
|
d5d516ce18 |
41
.github/workflows/deploy-netlify-ui.yml
vendored
Normal file
41
.github/workflows/deploy-netlify-ui.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: Deploy to Netlify (ui)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
# run this workflow only if an update is made to the ui package
|
||||||
|
paths:
|
||||||
|
- "packages/hoppscotch-ui/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup environment
|
||||||
|
run: mv .env.example .env
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v2.2.4
|
||||||
|
with:
|
||||||
|
version: 7
|
||||||
|
run_install: true
|
||||||
|
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
run: pnpm run generate-ui
|
||||||
|
|
||||||
|
# Deploy the ui site with netlify-cli
|
||||||
|
- name: Deploy to Netlify (ui)
|
||||||
|
run: npx netlify-cli deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
|
||||||
|
env:
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
[[headers]]
|
[[headers]]
|
||||||
for = "/*"
|
for = "/*"
|
||||||
[headers.values]
|
[headers.values]
|
||||||
X-Frame-Options = "DENY"
|
X-Frame-Options = "SAMEORIGIN"
|
||||||
X-XSS-Protection = "1; mode=block"
|
X-XSS-Protection = "1; mode=block"
|
||||||
|
|
||||||
[[redirects]]
|
[[redirects]]
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"typecheck": "pnpm -r do-typecheck",
|
"typecheck": "pnpm -r do-typecheck",
|
||||||
"lintfix": "pnpm -r do-lintfix",
|
"lintfix": "pnpm -r do-lintfix",
|
||||||
"pre-commit": "pnpm -r do-lint && pnpm -r do-typecheck",
|
"pre-commit": "pnpm -r do-lint && pnpm -r do-typecheck",
|
||||||
"test": "pnpm -r do-test"
|
"test": "pnpm -r do-test",
|
||||||
|
"generate-ui": "pnpm -r do-build-ui"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./packages/*"
|
"./packages/*"
|
||||||
|
|||||||
@@ -29,10 +29,7 @@ import IconCheck from "~icons/lucide/check"
|
|||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { platform } from "~/platform"
|
||||||
import { authIdToken$ } from "~/helpers/fb/auth"
|
|
||||||
|
|
||||||
const userAuthToken = useReadonlyStream(authIdToken$, null)
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -53,8 +50,9 @@ const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
|||||||
|
|
||||||
// Copy user auth token to clipboard
|
// Copy user auth token to clipboard
|
||||||
const copyUserAuthToken = () => {
|
const copyUserAuthToken = () => {
|
||||||
if (userAuthToken.value) {
|
const token = platform.auth.getDevOptsBackendIDToken()
|
||||||
copyToClipboard(userAuthToken.value)
|
if (token) {
|
||||||
|
copyToClipboard(token)
|
||||||
copyIcon.value = IconCheck
|
copyIcon.value = IconCheck
|
||||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ import { showChat } from "@modules/crisp"
|
|||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { currentUser$ } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
import { invokeAction } from "@helpers/actions"
|
import { invokeAction } from "@helpers/actions"
|
||||||
@@ -236,7 +236,10 @@ const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
|
|||||||
|
|
||||||
const navigatorShare = !!navigator.share
|
const navigatorShare = !!navigator.share
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(currentUser$, null)
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => ZEN_MODE.value,
|
() => ZEN_MODE.value,
|
||||||
|
|||||||
@@ -171,11 +171,10 @@ import IconUploadCloud from "~icons/lucide/upload-cloud"
|
|||||||
import IconUserPlus from "~icons/lucide/user-plus"
|
import IconUserPlus from "~icons/lucide/user-plus"
|
||||||
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
||||||
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
||||||
import { probableUser$ } from "@helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { invokeAction } from "@helpers/actions"
|
import { invokeAction } from "@helpers/actions"
|
||||||
import { platform } from "~/index"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -194,7 +193,10 @@ const mdAndLarger = breakpoints.greater("md")
|
|||||||
|
|
||||||
const network = reactive(useNetwork())
|
const network = reactive(useNetwork())
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(probableUser$, null)
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getProbableUserStream(),
|
||||||
|
platform.auth.getProbableUser()
|
||||||
|
)
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<any | null>(null)
|
const tippyActions = ref<any | null>(null)
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
|||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { currentUser$ } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
|
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
|
||||||
import { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
|
import { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
|
||||||
import { StepReturnValue } from "~/helpers/import-export/steps"
|
import { StepReturnValue } from "~/helpers/import-export/steps"
|
||||||
@@ -263,7 +263,10 @@ watch(inputChooseGistToImportFrom, (url) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const myCollections = useReadonlyStream(restCollections$, [])
|
const myCollections = useReadonlyStream(restCollections$, [])
|
||||||
const currentUser = useReadonlyStream(currentUser$, null)
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
const importerAction = async (stepResults: StepReturnValue[]) => {
|
const importerAction = async (stepResults: StepReturnValue[]) => {
|
||||||
if (!importerModule.value) return
|
if (!importerModule.value) return
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ import IconFolderPlus from "~icons/lucide/folder-plus"
|
|||||||
import IconDownload from "~icons/lucide/download"
|
import IconDownload from "~icons/lucide/download"
|
||||||
import IconGithub from "~icons/lucide/github"
|
import IconGithub from "~icons/lucide/github"
|
||||||
import { computed, ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
import { currentUser$ } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
@@ -120,7 +120,10 @@ const emit = defineEmits<{
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const collections = useReadonlyStream(graphqlCollections$, [])
|
const collections = useReadonlyStream(graphqlCollections$, [])
|
||||||
const currentUser = useReadonlyStream(currentUser$, null)
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<any | null>(null)
|
const tippyActions = ref<any | null>(null)
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ import {
|
|||||||
} from "~/helpers/backend/helpers"
|
} from "~/helpers/backend/helpers"
|
||||||
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { currentUser$ } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
import { createCollectionGists } from "~/helpers/gist"
|
import { createCollectionGists } from "~/helpers/gist"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
|
||||||
@@ -318,7 +318,10 @@ const confirmModalTitle = ref<string | null>(null)
|
|||||||
|
|
||||||
const filterTexts = ref("")
|
const filterTexts = ref("")
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(currentUser$, null)
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
const myCollections = useReadonlyStream(restCollections$, [], "deep")
|
const myCollections = useReadonlyStream(restCollections$, [], "deep")
|
||||||
|
|
||||||
// Export - Import refs
|
// Export - Import refs
|
||||||
@@ -1462,7 +1465,7 @@ const createCollectionGist = async () => {
|
|||||||
(result) => {
|
(result) => {
|
||||||
toast.success(t("export.gist_created").toString())
|
toast.success(t("export.gist_created").toString())
|
||||||
creatingGistCollection.value = false
|
creatingGistCollection.value = false
|
||||||
window.open(result.data.url)
|
window.open(result.data.html_url)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)()
|
)()
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
import { nextTick, ref, watch } from "vue"
|
import { nextTick, ref, watch } from "vue"
|
||||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
import { onLoggedIn } from "@composables/auth"
|
import { onLoggedIn } from "@composables/auth"
|
||||||
import { currentUserInfo$ } from "~/helpers/teams/BackendUserInfo"
|
import { platform } from "~/platform"
|
||||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useLocalState } from "~/newstore/localstate"
|
import { useLocalState } from "~/newstore/localstate"
|
||||||
@@ -111,7 +111,10 @@ const emit = defineEmits<{
|
|||||||
(e: "update-selected-team", team: SelectedTeam): void
|
(e: "update-selected-team", team: SelectedTeam): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(currentUserInfo$, null)
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
const adapter = new TeamListAdapter(true)
|
const adapter = new TeamListAdapter(true)
|
||||||
const myTeams = useReadonlyStream(adapter.teamList$, null)
|
const myTeams = useReadonlyStream(adapter.teamList$, null)
|
||||||
@@ -138,7 +141,9 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onLoggedIn(() => {
|
onLoggedIn(() => {
|
||||||
adapter.initialize()
|
try {
|
||||||
|
adapter.initialize()
|
||||||
|
} catch (e) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
const onTeamSelectIntersect = () => {
|
const onTeamSelectIntersect = () => {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ import IconDownload from "~icons/lucide/download"
|
|||||||
import IconGithub from "~icons/lucide/github"
|
import IconGithub from "~icons/lucide/github"
|
||||||
import { computed, ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
import { Environment } from "@hoppscotch/data"
|
import { Environment } from "@hoppscotch/data"
|
||||||
import { currentUser$ } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
@@ -141,7 +141,10 @@ const t = useI18n()
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const myEnvironments = useReadonlyStream(environments$, [])
|
const myEnvironments = useReadonlyStream(environments$, [])
|
||||||
const currentUser = useReadonlyStream(currentUser$, null)
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<TippyComponent | null>(null)
|
const tippyActions = ref<TippyComponent | null>(null)
|
||||||
@@ -187,7 +190,7 @@ const createEnvironmentGist = async () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
toast.success(t("export.gist_created").toString())
|
toast.success(t("export.gist_created").toString())
|
||||||
window.open(res.html_url)
|
window.open(res.data.html_url)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(t("error.something_went_wrong").toString())
|
toast.error(t("error.something_went_wrong").toString())
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|||||||
@@ -183,7 +183,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from "vue"
|
import { computed, ref, watch } from "vue"
|
||||||
import { isEqual } from "lodash-es"
|
import { isEqual } from "lodash-es"
|
||||||
import { currentUser$ } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
import { Team } from "~/helpers/backend/graphql"
|
import { Team } from "~/helpers/backend/graphql"
|
||||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
@@ -222,7 +222,10 @@ const globalEnvironment = computed(() => ({
|
|||||||
variables: globalEnv.value,
|
variables: globalEnv.value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(currentUser$, null)
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
const updateSelectedTeam = (newSelectedTeam: SelectedTeam) => {
|
const updateSelectedTeam = (newSelectedTeam: SelectedTeam) => {
|
||||||
environmentType.value.selectedTeam = newSelectedTeam
|
environmentType.value.selectedTeam = newSelectedTeam
|
||||||
|
|||||||
@@ -122,16 +122,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue"
|
import { defineComponent } from "vue"
|
||||||
import {
|
import { platform } from "~/platform"
|
||||||
signInUserWithGoogle,
|
|
||||||
signInUserWithGithub,
|
|
||||||
signInUserWithMicrosoft,
|
|
||||||
setProviderInfo,
|
|
||||||
currentUser$,
|
|
||||||
signInWithEmail,
|
|
||||||
linkWithFBCredentialFromAuthError,
|
|
||||||
getGithubCredentialFromResult,
|
|
||||||
} from "~/helpers/fb/auth"
|
|
||||||
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 IconEmail from "~icons/auth/email"
|
||||||
@@ -174,6 +165,8 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||||
|
|
||||||
this.subscribeToStream(currentUser$, (user) => {
|
this.subscribeToStream(currentUser$, (user) => {
|
||||||
if (user) this.hideModal()
|
if (user) this.hideModal()
|
||||||
})
|
})
|
||||||
@@ -186,8 +179,7 @@ export default defineComponent({
|
|||||||
this.signingInWithGoogle = true
|
this.signingInWithGoogle = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await signInUserWithGoogle()
|
await platform.auth.signInUserWithGoogle()
|
||||||
this.showLoginSuccess()
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
/*
|
/*
|
||||||
@@ -202,35 +194,32 @@ export default defineComponent({
|
|||||||
async signInWithGithub() {
|
async signInWithGithub() {
|
||||||
this.signingInWithGitHub = true
|
this.signingInWithGitHub = true
|
||||||
|
|
||||||
try {
|
const result = await platform.auth.signInUserWithGithub()
|
||||||
const result = await signInUserWithGithub()
|
|
||||||
const credential = getGithubCredentialFromResult(result)!
|
|
||||||
const token = credential.accessToken
|
|
||||||
setProviderInfo(result.providerId!, token!)
|
|
||||||
|
|
||||||
this.showLoginSuccess()
|
if (!result) {
|
||||||
} catch (e) {
|
this.signingInWithGitHub = false
|
||||||
console.error(e)
|
return
|
||||||
// This user's email is already present in Firebase but with other providers, namely Google or Microsoft
|
}
|
||||||
if (
|
|
||||||
(e as any).code === "auth/account-exists-with-different-credential"
|
|
||||||
) {
|
|
||||||
this.toast.info(`${this.t("auth.account_exists")}`, {
|
|
||||||
duration: 0,
|
|
||||||
closeOnSwipe: false,
|
|
||||||
action: {
|
|
||||||
text: `${this.t("action.yes")}`,
|
|
||||||
onClick: async (_, toastObject) => {
|
|
||||||
await linkWithFBCredentialFromAuthError(e)
|
|
||||||
this.showLoginSuccess()
|
|
||||||
|
|
||||||
toastObject.goAway(0)
|
if (result.type === "success") {
|
||||||
},
|
// this.showLoginSuccess()
|
||||||
|
} else if (result.type === "account-exists-with-different-cred") {
|
||||||
|
this.toast.info(`${this.t("auth.account_exists")}`, {
|
||||||
|
duration: 0,
|
||||||
|
closeOnSwipe: false,
|
||||||
|
action: {
|
||||||
|
text: `${this.t("action.yes")}`,
|
||||||
|
onClick: async (_, toastObject) => {
|
||||||
|
await result.link()
|
||||||
|
this.showLoginSuccess()
|
||||||
|
|
||||||
|
toastObject.goAway(0)
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
} else {
|
})
|
||||||
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
} else {
|
||||||
}
|
console.log("error logging into github", result.err)
|
||||||
|
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.signingInWithGitHub = false
|
this.signingInWithGitHub = false
|
||||||
@@ -239,8 +228,8 @@ export default defineComponent({
|
|||||||
this.signingInWithMicrosoft = true
|
this.signingInWithMicrosoft = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await signInUserWithMicrosoft()
|
await platform.auth.signInUserWithMicrosoft()
|
||||||
this.showLoginSuccess()
|
// this.showLoginSuccess()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
/*
|
/*
|
||||||
@@ -259,11 +248,8 @@ export default defineComponent({
|
|||||||
async signInWithEmail() {
|
async signInWithEmail() {
|
||||||
this.signingInWithEmail = true
|
this.signingInWithEmail = true
|
||||||
|
|
||||||
const actionCodeSettings = {
|
await platform.auth
|
||||||
url: `${import.meta.env.VITE_BASE_URL}/enter`,
|
.signInWithEmail(this.form.email)
|
||||||
handleCodeInApp: true,
|
|
||||||
}
|
|
||||||
await signInWithEmail(this.form.email, actionCodeSettings)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.mode = "email-sent"
|
this.mode = "email-sent"
|
||||||
setLocalConfig("emailForSignIn", this.form.email)
|
setLocalConfig("emailForSignIn", this.form.email)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { ref } from "vue"
|
|||||||
import IconLogOut from "~icons/lucide/log-out"
|
import IconLogOut from "~icons/lucide/log-out"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { signOutUser } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
outline: {
|
outline: {
|
||||||
@@ -47,7 +47,7 @@ const t = useI18n()
|
|||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
try {
|
try {
|
||||||
await signOutUser()
|
await platform.auth.signOutUser()
|
||||||
toast.success(`${t("auth.logged_out")}`)
|
toast.success(`${t("auth.logged_out")}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ import { ref, watchEffect, computed } from "vue"
|
|||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import * as TE from "fp-ts/TaskEither"
|
import * as TE from "fp-ts/TaskEither"
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
import { currentUser$ } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
import { onAuthEvent, onLoggedIn } from "@composables/auth"
|
import { onAuthEvent, onLoggedIn } from "@composables/auth"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
@@ -102,7 +102,10 @@ usePageHead({
|
|||||||
title: computed(() => t("navigation.profile")),
|
title: computed(() => t("navigation.profile")),
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(currentUser$, null)
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
const displayName = ref(currentUser.value?.displayName)
|
const displayName = ref(currentUser.value?.displayName)
|
||||||
watchEffect(() => (displayName.value = currentUser.value?.displayName))
|
watchEffect(() => (displayName.value = currentUser.value?.displayName))
|
||||||
@@ -121,7 +124,9 @@ const loading = computed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onLoggedIn(() => {
|
onLoggedIn(() => {
|
||||||
adapter.initialize()
|
try {
|
||||||
|
adapter.initialize()
|
||||||
|
} catch (e) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
onAuthEvent((ev) => {
|
onAuthEvent((ev) => {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ import { useI18n } from "~/composables/i18n"
|
|||||||
import { useToast } from "~/composables/toast"
|
import { useToast } from "~/composables/toast"
|
||||||
import { GetMyTeamsDocument, GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
import { GetMyTeamsDocument, GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
import { deleteUser } from "~/helpers/backend/mutations/Profile"
|
import { deleteUser } from "~/helpers/backend/mutations/Profile"
|
||||||
import { signOutUser } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -162,7 +162,7 @@ const deleteUserAccount = async () => {
|
|||||||
deletingUser.value = false
|
deletingUser.value = false
|
||||||
showDeleteAccountModal.value = false
|
showDeleteAccountModal.value = false
|
||||||
toast.success(t("settings.account_deleted"))
|
toast.success(t("settings.account_deleted"))
|
||||||
signOutUser()
|
platform.auth.signOutUser()
|
||||||
router.push(`/`)
|
router.push(`/`)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ const loading = computed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onLoggedIn(() => {
|
onLoggedIn(() => {
|
||||||
adapter.initialize()
|
try {
|
||||||
|
adapter.initialize()
|
||||||
|
} catch (e) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
import {
|
import { platform } from "~/platform"
|
||||||
currentUser$,
|
import { AuthEvent, HoppUser } from "~/platform/auth"
|
||||||
HoppUser,
|
import { Subscription } from "rxjs"
|
||||||
AuthEvent,
|
import { onBeforeUnmount, onMounted, watch, WatchStopHandle } from "vue"
|
||||||
authEvents$,
|
import { useReadonlyStream } from "./stream"
|
||||||
authIdToken$,
|
|
||||||
} from "@helpers/fb/auth"
|
|
||||||
import {
|
|
||||||
map,
|
|
||||||
distinctUntilChanged,
|
|
||||||
filter,
|
|
||||||
Subscription,
|
|
||||||
combineLatestWith,
|
|
||||||
} from "rxjs"
|
|
||||||
import { onBeforeUnmount, onMounted } from "vue"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Vue composable function that is called when the auth status
|
* A Vue composable function that is called when the auth status
|
||||||
@@ -21,26 +11,25 @@ import { onBeforeUnmount, onMounted } from "vue"
|
|||||||
* was already resolved before mount.
|
* was already resolved before mount.
|
||||||
*/
|
*/
|
||||||
export function onLoggedIn(exec: (user: HoppUser) => void) {
|
export function onLoggedIn(exec: (user: HoppUser) => void) {
|
||||||
let sub: Subscription | null = null
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
|
let watchStop: WatchStopHandle | null = null
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
sub = currentUser$
|
if (currentUser.value) exec(currentUser.value)
|
||||||
.pipe(
|
|
||||||
// We don't consider the state as logged in unless we also have an id token
|
watchStop = watch(currentUser, (newVal, prev) => {
|
||||||
combineLatestWith(authIdToken$),
|
if (prev === null && newVal !== null) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
exec(newVal)
|
||||||
filter(([_, token]) => !!token),
|
}
|
||||||
map((user) => !!user), // Get a logged in status (true or false)
|
})
|
||||||
distinctUntilChanged(), // Don't propagate unless the status updates
|
|
||||||
filter((x) => x) // Don't propagate unless it is logged in
|
|
||||||
)
|
|
||||||
.subscribe(() => {
|
|
||||||
exec(currentUser$.value!)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
sub?.unsubscribe()
|
watchStop?.()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +46,8 @@ export function onLoggedIn(exec: (user: HoppUser) => void) {
|
|||||||
* @param func A function which accepts an event
|
* @param func A function which accepts an event
|
||||||
*/
|
*/
|
||||||
export function onAuthEvent(func: (ev: AuthEvent) => void) {
|
export function onAuthEvent(func: (ev: AuthEvent) => void) {
|
||||||
|
const authEvents$ = platform.auth.getAuthEventsStream()
|
||||||
|
|
||||||
let sub: Subscription | null = null
|
let sub: Subscription | null = null
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
CombinedError,
|
CombinedError,
|
||||||
Operation,
|
Operation,
|
||||||
OperationResult,
|
OperationResult,
|
||||||
|
Client,
|
||||||
} from "@urql/core"
|
} from "@urql/core"
|
||||||
import { authExchange } from "@urql/exchange-auth"
|
import { authExchange } from "@urql/exchange-auth"
|
||||||
import { devtoolsExchange } from "@urql/devtools"
|
import { devtoolsExchange } from "@urql/devtools"
|
||||||
@@ -21,12 +22,7 @@ import * as TE from "fp-ts/TaskEither"
|
|||||||
import { pipe, constVoid, flow } from "fp-ts/function"
|
import { pipe, constVoid, flow } from "fp-ts/function"
|
||||||
import { subscribe, pipe as wonkaPipe } from "wonka"
|
import { subscribe, pipe as wonkaPipe } from "wonka"
|
||||||
import { filter, map, Subject } from "rxjs"
|
import { filter, map, Subject } from "rxjs"
|
||||||
import {
|
import { platform } from "~/platform"
|
||||||
authIdToken$,
|
|
||||||
getAuthIDToken,
|
|
||||||
probableUser$,
|
|
||||||
waitProbableLoginToConfirm,
|
|
||||||
} from "~/helpers/fb/auth"
|
|
||||||
|
|
||||||
// TODO: Implement caching
|
// TODO: Implement caching
|
||||||
|
|
||||||
@@ -57,11 +53,7 @@ export const gqlClientError$ = new Subject<GQLClientErrorEvent>()
|
|||||||
const createSubscriptionClient = () => {
|
const createSubscriptionClient = () => {
|
||||||
return new SubscriptionClient(BACKEND_WS_URL, {
|
return new SubscriptionClient(BACKEND_WS_URL, {
|
||||||
reconnect: true,
|
reconnect: true,
|
||||||
connectionParams: () => {
|
connectionParams: () => platform.auth.getBackendHeaders(),
|
||||||
return {
|
|
||||||
authorization: `Bearer ${authIdToken$.value}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
connectionCallback(error) {
|
connectionCallback(error) {
|
||||||
if (error?.length > 0) {
|
if (error?.length > 0) {
|
||||||
gqlClientError$.next({
|
gqlClientError$.next({
|
||||||
@@ -79,7 +71,7 @@ const createHoppClient = () => {
|
|||||||
dedupExchange,
|
dedupExchange,
|
||||||
authExchange({
|
authExchange({
|
||||||
addAuthToOperation({ authState, operation }) {
|
addAuthToOperation({ authState, operation }) {
|
||||||
if (!authState || !authState.authToken) {
|
if (!authState) {
|
||||||
return operation
|
return operation
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,28 +80,29 @@ const createHoppClient = () => {
|
|||||||
? operation.context.fetchOptions()
|
? operation.context.fetchOptions()
|
||||||
: operation.context.fetchOptions || {}
|
: operation.context.fetchOptions || {}
|
||||||
|
|
||||||
|
const authHeaders = platform.auth.getBackendHeaders()
|
||||||
|
|
||||||
return makeOperation(operation.kind, operation, {
|
return makeOperation(operation.kind, operation, {
|
||||||
...operation.context,
|
...operation.context,
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
...fetchOptions,
|
...fetchOptions,
|
||||||
headers: {
|
headers: {
|
||||||
...fetchOptions.headers,
|
...fetchOptions.headers,
|
||||||
Authorization: `Bearer ${authState.authToken}`,
|
...authHeaders,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
willAuthError({ authState }) {
|
willAuthError() {
|
||||||
return !authState || !authState.authToken
|
return platform.auth.willBackendHaveAuthError()
|
||||||
},
|
},
|
||||||
getAuth: async () => {
|
getAuth: async () => {
|
||||||
if (!probableUser$.value) return { authToken: null }
|
const probableUser = platform.auth.getProbableUser()
|
||||||
|
|
||||||
await waitProbableLoginToConfirm()
|
if (probableUser !== null)
|
||||||
|
await platform.auth.waitProbableLoginToConfirm()
|
||||||
|
|
||||||
return {
|
return {}
|
||||||
authToken: getAuthIDToken(),
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
fetchExchange,
|
fetchExchange,
|
||||||
@@ -137,31 +130,40 @@ const createHoppClient = () => {
|
|||||||
return createClient({
|
return createClient({
|
||||||
url: BACKEND_GQL_URL,
|
url: BACKEND_GQL_URL,
|
||||||
exchanges,
|
exchanges,
|
||||||
|
...(platform.auth.getGQLClientOptions
|
||||||
|
? platform.auth.getGQLClientOptions()
|
||||||
|
: {}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let subscriptionClient: SubscriptionClient | null
|
let subscriptionClient: SubscriptionClient | null
|
||||||
export const client = ref(createHoppClient())
|
export const client = ref<Client>()
|
||||||
|
|
||||||
authIdToken$.subscribe((idToken) => {
|
|
||||||
// triggering reconnect by closing the websocket client
|
|
||||||
if (idToken && subscriptionClient) {
|
|
||||||
subscriptionClient?.client?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// creating new subscription
|
|
||||||
if (idToken && !subscriptionClient) {
|
|
||||||
subscriptionClient = createSubscriptionClient()
|
|
||||||
}
|
|
||||||
|
|
||||||
// closing existing subscription client.
|
|
||||||
if (!idToken && subscriptionClient) {
|
|
||||||
subscriptionClient.close()
|
|
||||||
subscriptionClient = null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export function initBackendGQLClient() {
|
||||||
client.value = createHoppClient()
|
client.value = createHoppClient()
|
||||||
})
|
|
||||||
|
platform.auth.onBackendGQLClientShouldReconnect(() => {
|
||||||
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
|
// triggering reconnect by closing the websocket client
|
||||||
|
if (currentUser && subscriptionClient) {
|
||||||
|
subscriptionClient?.client?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// creating new subscription
|
||||||
|
if (currentUser && !subscriptionClient) {
|
||||||
|
subscriptionClient = createSubscriptionClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
// closing existing subscription client.
|
||||||
|
if (!currentUser && subscriptionClient) {
|
||||||
|
subscriptionClient.close()
|
||||||
|
subscriptionClient = null
|
||||||
|
}
|
||||||
|
|
||||||
|
client.value = createHoppClient()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type RunQueryOptions<T = any, V = object> = {
|
type RunQueryOptions<T = any, V = object> = {
|
||||||
query: TypedDocumentNode<T, V>
|
query: TypedDocumentNode<T, V>
|
||||||
@@ -185,7 +187,7 @@ export const runGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
|
|||||||
args: RunQueryOptions<DocType, DocVarType>
|
args: RunQueryOptions<DocType, DocVarType>
|
||||||
): Promise<E.Either<GQLError<DocErrorType>, DocType>> => {
|
): Promise<E.Either<GQLError<DocErrorType>, DocType>> => {
|
||||||
const request = createRequest<DocType, DocVarType>(args.query, args.variables)
|
const request = createRequest<DocType, DocVarType>(args.query, args.variables)
|
||||||
const source = client.value.executeQuery(request, {
|
const source = client.value!.executeQuery(request, {
|
||||||
requestPolicy: "network-only",
|
requestPolicy: "network-only",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -250,7 +252,7 @@ export const runGQLSubscription = <
|
|||||||
) => {
|
) => {
|
||||||
const result$ = new Subject<E.Either<GQLError<DocErrorType>, DocType>>()
|
const result$ = new Subject<E.Either<GQLError<DocErrorType>, DocType>>()
|
||||||
|
|
||||||
const source = client.value.executeSubscription(
|
const source = client.value!.executeSubscription(
|
||||||
createRequest(args.query, args.variables)
|
createRequest(args.query, args.variables)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -342,8 +344,8 @@ export const runMutation = <
|
|||||||
pipe(
|
pipe(
|
||||||
TE.tryCatch(
|
TE.tryCatch(
|
||||||
() =>
|
() =>
|
||||||
client.value
|
client
|
||||||
.mutation(mutation, variables, {
|
.value!.mutation(mutation, variables, {
|
||||||
requestPolicy: "cache-and-network",
|
requestPolicy: "cache-and-network",
|
||||||
...additionalConfig,
|
...additionalConfig,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
setUserId,
|
setUserId,
|
||||||
setUserProperties,
|
setUserProperties,
|
||||||
} from "firebase/analytics"
|
} from "firebase/analytics"
|
||||||
import { authEvents$ } from "./auth"
|
import { platform } from "~/platform"
|
||||||
import {
|
import {
|
||||||
HoppAccentColor,
|
HoppAccentColor,
|
||||||
HoppBgColor,
|
HoppBgColor,
|
||||||
@@ -42,13 +42,15 @@ export function initAnalytics() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initLoginListeners() {
|
function initLoginListeners() {
|
||||||
|
const authEvents$ = platform.auth.getAuthEventsStream()
|
||||||
|
|
||||||
authEvents$.subscribe((ev) => {
|
authEvents$.subscribe((ev) => {
|
||||||
if (ev.event === "login") {
|
if (ev.event === "login") {
|
||||||
if (settingsStore.value.TELEMETRY_ENABLED && analytics) {
|
if (settingsStore.value.TELEMETRY_ENABLED && analytics) {
|
||||||
setUserId(analytics, ev.user.uid)
|
setUserId(analytics, ev.user.uid)
|
||||||
|
|
||||||
logEvent(analytics, "login", {
|
logEvent(analytics, "login", {
|
||||||
method: ev.user.providerData[0]?.providerId, // Assume the first provider is the login provider
|
method: ev.user.provider, // Assume the first provider is the login provider
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (ev.event === "logout") {
|
} else if (ev.event === "logout") {
|
||||||
|
|||||||
@@ -1,434 +0,0 @@
|
|||||||
import {
|
|
||||||
User,
|
|
||||||
getAuth,
|
|
||||||
onAuthStateChanged,
|
|
||||||
onIdTokenChanged,
|
|
||||||
signInWithPopup,
|
|
||||||
GoogleAuthProvider,
|
|
||||||
GithubAuthProvider,
|
|
||||||
OAuthProvider,
|
|
||||||
signInWithEmailAndPassword as signInWithEmailAndPass,
|
|
||||||
isSignInWithEmailLink as isSignInWithEmailLinkFB,
|
|
||||||
fetchSignInMethodsForEmail,
|
|
||||||
sendSignInLinkToEmail,
|
|
||||||
signInWithEmailLink as signInWithEmailLinkFB,
|
|
||||||
ActionCodeSettings,
|
|
||||||
signOut,
|
|
||||||
linkWithCredential,
|
|
||||||
AuthCredential,
|
|
||||||
AuthError,
|
|
||||||
UserCredential,
|
|
||||||
updateProfile,
|
|
||||||
updateEmail,
|
|
||||||
sendEmailVerification,
|
|
||||||
reauthenticateWithCredential,
|
|
||||||
} from "firebase/auth"
|
|
||||||
import {
|
|
||||||
onSnapshot,
|
|
||||||
getFirestore,
|
|
||||||
setDoc,
|
|
||||||
doc,
|
|
||||||
updateDoc,
|
|
||||||
} from "firebase/firestore"
|
|
||||||
import { BehaviorSubject, filter, Subject, Subscription } from "rxjs"
|
|
||||||
import {
|
|
||||||
setLocalConfig,
|
|
||||||
getLocalConfig,
|
|
||||||
removeLocalConfig,
|
|
||||||
} from "~/newstore/localpersistence"
|
|
||||||
|
|
||||||
export type HoppUser = User & {
|
|
||||||
provider?: string
|
|
||||||
accessToken?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AuthEvent =
|
|
||||||
| { event: "probable_login"; user: HoppUser } // We have previous login state, but the app is waiting for authentication
|
|
||||||
| { event: "login"; user: HoppUser } // We are authenticated
|
|
||||||
| { event: "logout" } // No authentication and we have no previous state
|
|
||||||
| { event: "authTokenUpdate"; user: HoppUser; newToken: string | null } // Token has been updated
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A BehaviorSubject emitting the currently logged in user (or null if not logged in)
|
|
||||||
*/
|
|
||||||
export const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
|
||||||
/**
|
|
||||||
* A BehaviorSubject emitting the current idToken
|
|
||||||
*/
|
|
||||||
export const authIdToken$ = new BehaviorSubject<string | null>(null)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A subject that emits events related to authentication flows
|
|
||||||
*/
|
|
||||||
export const authEvents$ = new Subject<AuthEvent>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like currentUser$ but also gives probable user value
|
|
||||||
*/
|
|
||||||
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves when the probable login resolves into proper login
|
|
||||||
*/
|
|
||||||
export const waitProbableLoginToConfirm = () =>
|
|
||||||
new Promise<void>((resolve, reject) => {
|
|
||||||
if (authIdToken$.value) resolve()
|
|
||||||
|
|
||||||
if (!probableUser$.value) reject(new Error("no_probable_user"))
|
|
||||||
|
|
||||||
let sub: Subscription | null = null
|
|
||||||
|
|
||||||
sub = authIdToken$.pipe(filter((token) => !!token)).subscribe(() => {
|
|
||||||
sub?.unsubscribe()
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the firebase authentication related subjects
|
|
||||||
*/
|
|
||||||
export function initAuth() {
|
|
||||||
const auth = getAuth()
|
|
||||||
const firestore = getFirestore()
|
|
||||||
|
|
||||||
let extraSnapshotStop: (() => void) | null = null
|
|
||||||
|
|
||||||
probableUser$.next(JSON.parse(getLocalConfig("login_state") ?? "null"))
|
|
||||||
|
|
||||||
onAuthStateChanged(auth, (user) => {
|
|
||||||
/** Whether the user was logged in before */
|
|
||||||
const wasLoggedIn = currentUser$.value !== null
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
probableUser$.next(user)
|
|
||||||
} else {
|
|
||||||
probableUser$.next(null)
|
|
||||||
removeLocalConfig("login_state")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user && extraSnapshotStop) {
|
|
||||||
extraSnapshotStop()
|
|
||||||
extraSnapshotStop = null
|
|
||||||
} else if (user) {
|
|
||||||
// Merge all the user info from all the authenticated providers
|
|
||||||
user.providerData.forEach((profile) => {
|
|
||||||
if (!profile) return
|
|
||||||
|
|
||||||
const us = {
|
|
||||||
updatedOn: new Date(),
|
|
||||||
provider: profile.providerId,
|
|
||||||
name: profile.displayName,
|
|
||||||
email: profile.email,
|
|
||||||
photoUrl: profile.photoURL,
|
|
||||||
uid: profile.uid,
|
|
||||||
}
|
|
||||||
|
|
||||||
setDoc(doc(firestore, "users", user.uid), us, { merge: true }).catch(
|
|
||||||
(e) => console.error("error updating", us, e)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
extraSnapshotStop = onSnapshot(
|
|
||||||
doc(firestore, "users", user.uid),
|
|
||||||
(doc) => {
|
|
||||||
const data = doc.data()
|
|
||||||
|
|
||||||
const userUpdate: HoppUser = user
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
// Write extra provider data
|
|
||||||
userUpdate.provider = data.provider
|
|
||||||
userUpdate.accessToken = data.accessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUser$.next(userUpdate)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
currentUser$.next(user)
|
|
||||||
|
|
||||||
// User wasn't found before, but now is there (login happened)
|
|
||||||
if (!wasLoggedIn && user) {
|
|
||||||
authEvents$.next({
|
|
||||||
event: "login",
|
|
||||||
user: currentUser$.value!,
|
|
||||||
})
|
|
||||||
} else if (wasLoggedIn && !user) {
|
|
||||||
// User was found before, but now is not there (logout happened)
|
|
||||||
authEvents$.next({
|
|
||||||
event: "logout",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onIdTokenChanged(auth, async (user) => {
|
|
||||||
if (user) {
|
|
||||||
authIdToken$.next(await user.getIdToken())
|
|
||||||
|
|
||||||
authEvents$.next({
|
|
||||||
event: "authTokenUpdate",
|
|
||||||
newToken: authIdToken$.value,
|
|
||||||
user: currentUser$.value!, // Force not-null because user is defined
|
|
||||||
})
|
|
||||||
|
|
||||||
setLocalConfig("login_state", JSON.stringify(user))
|
|
||||||
} else {
|
|
||||||
authIdToken$.next(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAuthIDToken(): string | null {
|
|
||||||
return authIdToken$.getValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign user in with a popup using Google
|
|
||||||
*/
|
|
||||||
export async function signInUserWithGoogle() {
|
|
||||||
return await signInWithPopup(getAuth(), new GoogleAuthProvider())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign user in with a popup using Github
|
|
||||||
*/
|
|
||||||
export async function signInUserWithGithub() {
|
|
||||||
return await signInWithPopup(
|
|
||||||
getAuth(),
|
|
||||||
new GithubAuthProvider().addScope("gist")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign user in with a popup using Microsoft
|
|
||||||
*/
|
|
||||||
export async function signInUserWithMicrosoft() {
|
|
||||||
return await signInWithPopup(getAuth(), new OAuthProvider("microsoft.com"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign user in with email and password
|
|
||||||
*/
|
|
||||||
export async function signInWithEmailAndPassword(
|
|
||||||
email: string,
|
|
||||||
password: string
|
|
||||||
) {
|
|
||||||
return await signInWithEmailAndPass(getAuth(), email, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the sign in methods for a given email address
|
|
||||||
*
|
|
||||||
* @param email - Email to get the methods of
|
|
||||||
*
|
|
||||||
* @returns Promise for string array of the auth provider methods accessible
|
|
||||||
*/
|
|
||||||
export async function getSignInMethodsForEmail(email: string) {
|
|
||||||
return await fetchSignInMethodsForEmail(getAuth(), email)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function linkWithFBCredential(
|
|
||||||
user: User,
|
|
||||||
credential: AuthCredential
|
|
||||||
) {
|
|
||||||
return await linkWithCredential(user, credential)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Links account with another account given in a auth/account-exists-with-different-credential error
|
|
||||||
*
|
|
||||||
* @param error - Error caught after trying to login
|
|
||||||
*
|
|
||||||
* @returns Promise of UserCredential
|
|
||||||
*/
|
|
||||||
export async function linkWithFBCredentialFromAuthError(error: unknown) {
|
|
||||||
// credential is not null since this function is called after an auth/account-exists-with-different-credential error, ie credentials actually exist
|
|
||||||
const credentials = OAuthProvider.credentialFromError(error as AuthError)!
|
|
||||||
|
|
||||||
const otherLinkedProviders = (
|
|
||||||
await getSignInMethodsForEmail((error as AuthError).customData.email!)
|
|
||||||
).filter((providerId) => credentials.providerId !== providerId)
|
|
||||||
|
|
||||||
let user: User | null = null
|
|
||||||
|
|
||||||
if (otherLinkedProviders.indexOf("google.com") >= -1) {
|
|
||||||
user = (await signInUserWithGoogle()).user
|
|
||||||
} else if (otherLinkedProviders.indexOf("github.com") >= -1) {
|
|
||||||
user = (await signInUserWithGithub()).user
|
|
||||||
} else if (otherLinkedProviders.indexOf("microsoft.com") >= -1) {
|
|
||||||
user = (await signInUserWithMicrosoft()).user
|
|
||||||
}
|
|
||||||
|
|
||||||
// user is not null since going through each provider will return a user
|
|
||||||
return await linkWithCredential(user!, credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an email with the signin link to the user
|
|
||||||
*
|
|
||||||
* @param email - Email to send the email to
|
|
||||||
* @param actionCodeSettings - The settings to apply to the link
|
|
||||||
*/
|
|
||||||
export async function signInWithEmail(
|
|
||||||
email: string,
|
|
||||||
actionCodeSettings: ActionCodeSettings
|
|
||||||
) {
|
|
||||||
return await sendSignInLinkToEmail(getAuth(), email, actionCodeSettings)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks and returns whether the sign in link is an email link
|
|
||||||
*
|
|
||||||
* @param url - The URL to look in
|
|
||||||
*/
|
|
||||||
export function isSignInWithEmailLink(url: string) {
|
|
||||||
return isSignInWithEmailLinkFB(getAuth(), url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an email with sign in with email link
|
|
||||||
*
|
|
||||||
* @param email - Email to log in to
|
|
||||||
* @param url - The action URL which is used to validate login
|
|
||||||
*/
|
|
||||||
export async function signInWithEmailLink(email: string, url: string) {
|
|
||||||
return await signInWithEmailLinkFB(getAuth(), email, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signs out the user
|
|
||||||
*/
|
|
||||||
export async function signOutUser() {
|
|
||||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
|
||||||
|
|
||||||
await signOut(getAuth())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the provider id and relevant provider auth token
|
|
||||||
* as user metadata
|
|
||||||
*
|
|
||||||
* @param id - The provider ID
|
|
||||||
* @param token - The relevant auth token for the given provider
|
|
||||||
*/
|
|
||||||
export async function setProviderInfo(id: string, token: string) {
|
|
||||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
|
||||||
|
|
||||||
const us = {
|
|
||||||
updatedOn: new Date(),
|
|
||||||
provider: id,
|
|
||||||
accessToken: token,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateDoc(
|
|
||||||
doc(getFirestore(), "users", currentUser$.value.uid),
|
|
||||||
us
|
|
||||||
).catch((e) => console.error("error updating", us, e))
|
|
||||||
} catch (e) {
|
|
||||||
console.error("error updating", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the user's display name
|
|
||||||
*
|
|
||||||
* @param name - The new display name
|
|
||||||
*/
|
|
||||||
export async function setDisplayName(name: string) {
|
|
||||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
|
||||||
|
|
||||||
const us = {
|
|
||||||
displayName: name,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateProfile(currentUser$.value, us)
|
|
||||||
} catch (e) {
|
|
||||||
console.error("error updating", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send user's email address verification mail
|
|
||||||
*/
|
|
||||||
export async function verifyEmailAddress() {
|
|
||||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
|
||||||
|
|
||||||
try {
|
|
||||||
await sendEmailVerification(currentUser$.value)
|
|
||||||
} catch (e) {
|
|
||||||
console.error("error updating", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the user's email address
|
|
||||||
*
|
|
||||||
* @param email - The new email address
|
|
||||||
*/
|
|
||||||
export async function setEmailAddress(email: string) {
|
|
||||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateEmail(currentUser$.value, email)
|
|
||||||
} catch (e) {
|
|
||||||
await reauthenticateUser()
|
|
||||||
console.error("error updating", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reauthenticate the user with the given credential
|
|
||||||
*/
|
|
||||||
async function reauthenticateUser() {
|
|
||||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
|
||||||
const currentAuthMethod = currentUser$.value.provider
|
|
||||||
let credential
|
|
||||||
if (currentAuthMethod === "google.com") {
|
|
||||||
const result = await signInUserWithGithub()
|
|
||||||
credential = GithubAuthProvider.credentialFromResult(result)
|
|
||||||
} else if (currentAuthMethod === "github.com") {
|
|
||||||
const result = await signInUserWithGoogle()
|
|
||||||
credential = GoogleAuthProvider.credentialFromResult(result)
|
|
||||||
} else if (currentAuthMethod === "microsoft.com") {
|
|
||||||
const result = await signInUserWithMicrosoft()
|
|
||||||
credential = OAuthProvider.credentialFromResult(result)
|
|
||||||
} else if (currentAuthMethod === "password") {
|
|
||||||
const email = prompt(
|
|
||||||
"Reauthenticate your account using your current email:"
|
|
||||||
)
|
|
||||||
const actionCodeSettings = {
|
|
||||||
url: `${process.env.BASE_URL}/enter`,
|
|
||||||
handleCodeInApp: true,
|
|
||||||
}
|
|
||||||
await signInWithEmail(email as string, actionCodeSettings)
|
|
||||||
.then(() =>
|
|
||||||
alert(
|
|
||||||
`Check your inbox - we sent an email to ${email}. It contains a magic link that will reauthenticate your account.`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.catch((e) => {
|
|
||||||
alert(`Error: ${e.message}`)
|
|
||||||
console.error(e)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await reauthenticateWithCredential(
|
|
||||||
currentUser$.value,
|
|
||||||
credential as AuthCredential
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
console.error("error updating", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGithubCredentialFromResult(result: UserCredential) {
|
|
||||||
return GithubAuthProvider.credentialFromResult(result)
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
translateToNewRESTCollection,
|
translateToNewRESTCollection,
|
||||||
translateToNewGQLCollection,
|
translateToNewGQLCollection,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import { currentUser$ } from "./auth"
|
import { platform } from "~/platform"
|
||||||
import {
|
import {
|
||||||
restCollections$,
|
restCollections$,
|
||||||
graphqlCollections$,
|
graphqlCollections$,
|
||||||
@@ -44,20 +44,22 @@ export async function writeCollections(
|
|||||||
collection: any[],
|
collection: any[],
|
||||||
flag: CollectionFlags
|
flag: CollectionFlags
|
||||||
) {
|
) {
|
||||||
if (currentUser$.value === null)
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
|
if (currentUser === null)
|
||||||
throw new Error("User not logged in to write collections")
|
throw new Error("User not logged in to write collections")
|
||||||
|
|
||||||
const cl = {
|
const cl = {
|
||||||
updatedOn: new Date(),
|
updatedOn: new Date(),
|
||||||
author: currentUser$.value.uid,
|
author: currentUser.uid,
|
||||||
author_name: currentUser$.value.displayName,
|
author_name: currentUser.displayName,
|
||||||
author_image: currentUser$.value.photoURL,
|
author_image: currentUser.photoURL,
|
||||||
collection,
|
collection,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setDoc(
|
await setDoc(
|
||||||
doc(getFirestore(), "users", currentUser$.value.uid, flag, "sync"),
|
doc(getFirestore(), "users", currentUser.uid, flag, "sync"),
|
||||||
cl
|
cl
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -67,10 +69,13 @@ export async function writeCollections(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initCollections() {
|
export function initCollections() {
|
||||||
|
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||||
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
const restCollSub = restCollections$.subscribe((collections) => {
|
const restCollSub = restCollections$.subscribe((collections) => {
|
||||||
if (
|
if (
|
||||||
loadedRESTCollections &&
|
loadedRESTCollections &&
|
||||||
currentUser$.value &&
|
currentUser &&
|
||||||
settingsStore.value.syncCollections
|
settingsStore.value.syncCollections
|
||||||
) {
|
) {
|
||||||
writeCollections(collections, "collections")
|
writeCollections(collections, "collections")
|
||||||
@@ -80,7 +85,7 @@ export function initCollections() {
|
|||||||
const gqlCollSub = graphqlCollections$.subscribe((collections) => {
|
const gqlCollSub = graphqlCollections$.subscribe((collections) => {
|
||||||
if (
|
if (
|
||||||
loadedGraphqlCollections &&
|
loadedGraphqlCollections &&
|
||||||
currentUser$.value &&
|
currentUser &&
|
||||||
settingsStore.value.syncCollections
|
settingsStore.value.syncCollections
|
||||||
) {
|
) {
|
||||||
writeCollections(collections, "collectionsGraphql")
|
writeCollections(collections, "collectionsGraphql")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
onSnapshot,
|
onSnapshot,
|
||||||
setDoc,
|
setDoc,
|
||||||
} from "firebase/firestore"
|
} from "firebase/firestore"
|
||||||
import { currentUser$ } from "./auth"
|
import { platform } from "~/platform"
|
||||||
import {
|
import {
|
||||||
environments$,
|
environments$,
|
||||||
globalEnv$,
|
globalEnv$,
|
||||||
@@ -32,26 +32,22 @@ let loadedEnvironments = false
|
|||||||
let loadedGlobals = true
|
let loadedGlobals = true
|
||||||
|
|
||||||
async function writeEnvironments(environment: Environment[]) {
|
async function writeEnvironments(environment: Environment[]) {
|
||||||
if (currentUser$.value == null)
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
|
if (currentUser === null)
|
||||||
throw new Error("Cannot write environments when signed out")
|
throw new Error("Cannot write environments when signed out")
|
||||||
|
|
||||||
const ev = {
|
const ev = {
|
||||||
updatedOn: new Date(),
|
updatedOn: new Date(),
|
||||||
author: currentUser$.value.uid,
|
author: currentUser.uid,
|
||||||
author_name: currentUser$.value.displayName,
|
author_name: currentUser.displayName,
|
||||||
author_image: currentUser$.value.photoURL,
|
author_image: currentUser.photoURL,
|
||||||
environment,
|
environment,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setDoc(
|
await setDoc(
|
||||||
doc(
|
doc(getFirestore(), "users", currentUser.uid, "environments", "sync"),
|
||||||
getFirestore(),
|
|
||||||
"users",
|
|
||||||
currentUser$.value.uid,
|
|
||||||
"environments",
|
|
||||||
"sync"
|
|
||||||
),
|
|
||||||
ev
|
ev
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -61,20 +57,22 @@ async function writeEnvironments(environment: Environment[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function writeGlobalEnvironment(variables: Environment["variables"]) {
|
async function writeGlobalEnvironment(variables: Environment["variables"]) {
|
||||||
if (currentUser$.value == null)
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
|
if (currentUser === null)
|
||||||
throw new Error("Cannot write global environment when signed out")
|
throw new Error("Cannot write global environment when signed out")
|
||||||
|
|
||||||
const ev = {
|
const ev = {
|
||||||
updatedOn: new Date(),
|
updatedOn: new Date(),
|
||||||
author: currentUser$.value.uid,
|
author: currentUser.uid,
|
||||||
author_name: currentUser$.value.displayName,
|
author_name: currentUser.displayName,
|
||||||
author_image: currentUser$.value.photoURL,
|
author_image: currentUser.photoURL,
|
||||||
variables,
|
variables,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setDoc(
|
await setDoc(
|
||||||
doc(getFirestore(), "users", currentUser$.value.uid, "globalEnv", "sync"),
|
doc(getFirestore(), "users", currentUser.uid, "globalEnv", "sync"),
|
||||||
ev
|
ev
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -84,9 +82,12 @@ async function writeGlobalEnvironment(variables: Environment["variables"]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initEnvironments() {
|
export function initEnvironments() {
|
||||||
|
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||||
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
const envListenSub = environments$.subscribe((envs) => {
|
const envListenSub = environments$.subscribe((envs) => {
|
||||||
if (
|
if (
|
||||||
currentUser$.value &&
|
currentUser &&
|
||||||
settingsStore.value.syncEnvironments &&
|
settingsStore.value.syncEnvironments &&
|
||||||
loadedEnvironments
|
loadedEnvironments
|
||||||
) {
|
) {
|
||||||
@@ -95,11 +96,7 @@ export function initEnvironments() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const globalListenSub = globalEnv$.subscribe((vars) => {
|
const globalListenSub = globalEnv$.subscribe((vars) => {
|
||||||
if (
|
if (currentUser && settingsStore.value.syncEnvironments && loadedGlobals) {
|
||||||
currentUser$.value &&
|
|
||||||
settingsStore.value.syncEnvironments &&
|
|
||||||
loadedGlobals
|
|
||||||
) {
|
|
||||||
writeGlobalEnvironment(vars)
|
writeGlobalEnvironment(vars)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
updateDoc,
|
updateDoc,
|
||||||
} from "firebase/firestore"
|
} from "firebase/firestore"
|
||||||
import { FormDataKeyValue } from "@hoppscotch/data"
|
import { FormDataKeyValue } from "@hoppscotch/data"
|
||||||
import { currentUser$ } from "./auth"
|
import { platform } from "~/platform"
|
||||||
import { getSettingSubject, settingsStore } from "~/newstore/settings"
|
import { getSettingSubject, settingsStore } from "~/newstore/settings"
|
||||||
import {
|
import {
|
||||||
GQLHistoryEntry,
|
GQLHistoryEntry,
|
||||||
@@ -76,7 +76,9 @@ async function writeHistory(
|
|||||||
? purgeFormDataFromRequest(entry as RESTHistoryEntry)
|
? purgeFormDataFromRequest(entry as RESTHistoryEntry)
|
||||||
: entry
|
: entry
|
||||||
|
|
||||||
if (currentUser$.value == null)
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
|
if (currentUser === null)
|
||||||
throw new Error("User not logged in to sync history")
|
throw new Error("User not logged in to sync history")
|
||||||
|
|
||||||
const hs = {
|
const hs = {
|
||||||
@@ -85,10 +87,7 @@ async function writeHistory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addDoc(
|
await addDoc(collection(getFirestore(), "users", currentUser.uid, col), hs)
|
||||||
collection(getFirestore(), "users", currentUser$.value.uid, col),
|
|
||||||
hs
|
|
||||||
)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("error writing to history", hs, e)
|
console.error("error writing to history", hs, e)
|
||||||
throw e
|
throw e
|
||||||
@@ -99,12 +98,14 @@ async function deleteHistory(
|
|||||||
entry: (RESTHistoryEntry | GQLHistoryEntry) & { id: string },
|
entry: (RESTHistoryEntry | GQLHistoryEntry) & { id: string },
|
||||||
col: HistoryFBCollections
|
col: HistoryFBCollections
|
||||||
) {
|
) {
|
||||||
if (currentUser$.value == null)
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
|
if (currentUser === null)
|
||||||
throw new Error("User not logged in to delete history")
|
throw new Error("User not logged in to delete history")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteDoc(
|
await deleteDoc(
|
||||||
doc(getFirestore(), "users", currentUser$.value.uid, col, entry.id)
|
doc(getFirestore(), "users", currentUser.uid, col, entry.id)
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("error deleting history", entry, e)
|
console.error("error deleting history", entry, e)
|
||||||
@@ -113,11 +114,13 @@ async function deleteHistory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function clearHistory(col: HistoryFBCollections) {
|
async function clearHistory(col: HistoryFBCollections) {
|
||||||
if (currentUser$.value == null)
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
|
if (currentUser === null)
|
||||||
throw new Error("User not logged in to clear history")
|
throw new Error("User not logged in to clear history")
|
||||||
|
|
||||||
const { docs } = await getDocs(
|
const { docs } = await getDocs(
|
||||||
collection(getFirestore(), "users", currentUser$.value.uid, col)
|
collection(getFirestore(), "users", currentUser.uid, col)
|
||||||
)
|
)
|
||||||
|
|
||||||
await Promise.all(docs.map((e) => deleteHistory(e as any, col)))
|
await Promise.all(docs.map((e) => deleteHistory(e as any, col)))
|
||||||
@@ -127,12 +130,13 @@ async function toggleStar(
|
|||||||
entry: (RESTHistoryEntry | GQLHistoryEntry) & { id: string },
|
entry: (RESTHistoryEntry | GQLHistoryEntry) & { id: string },
|
||||||
col: HistoryFBCollections
|
col: HistoryFBCollections
|
||||||
) {
|
) {
|
||||||
if (currentUser$.value == null)
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
throw new Error("User not logged in to toggle star")
|
|
||||||
|
if (currentUser === null) throw new Error("User not logged in to toggle star")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateDoc(
|
await updateDoc(
|
||||||
doc(getFirestore(), "users", currentUser$.value.uid, col, entry.id),
|
doc(getFirestore(), "users", currentUser.uid, col, entry.id),
|
||||||
{ star: !entry.star }
|
{ star: !entry.star }
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -142,12 +146,11 @@ async function toggleStar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initHistory() {
|
export function initHistory() {
|
||||||
|
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||||
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
const restHistorySub = restHistoryStore.dispatches$.subscribe((dispatch) => {
|
const restHistorySub = restHistoryStore.dispatches$.subscribe((dispatch) => {
|
||||||
if (
|
if (loadedRESTHistory && currentUser && settingsStore.value.syncHistory) {
|
||||||
loadedRESTHistory &&
|
|
||||||
currentUser$.value &&
|
|
||||||
settingsStore.value.syncHistory
|
|
||||||
) {
|
|
||||||
if (dispatch.dispatcher === "addEntry") {
|
if (dispatch.dispatcher === "addEntry") {
|
||||||
writeHistory(dispatch.payload.entry, "history")
|
writeHistory(dispatch.payload.entry, "history")
|
||||||
} else if (dispatch.dispatcher === "deleteEntry") {
|
} else if (dispatch.dispatcher === "deleteEntry") {
|
||||||
@@ -164,7 +167,7 @@ export function initHistory() {
|
|||||||
(dispatch) => {
|
(dispatch) => {
|
||||||
if (
|
if (
|
||||||
loadedGraphqlHistory &&
|
loadedGraphqlHistory &&
|
||||||
currentUser$.value &&
|
currentUser &&
|
||||||
settingsStore.value.syncHistory
|
settingsStore.value.syncHistory
|
||||||
) {
|
) {
|
||||||
if (dispatch.dispatcher === "addEntry") {
|
if (dispatch.dispatcher === "addEntry") {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { initializeApp } from "firebase/app"
|
import { initializeApp } from "firebase/app"
|
||||||
|
import { platform } from "~/platform"
|
||||||
import { initAnalytics } from "./analytics"
|
import { initAnalytics } from "./analytics"
|
||||||
import { initAuth } from "./auth"
|
|
||||||
import { initCollections } from "./collections"
|
import { initCollections } from "./collections"
|
||||||
import { initEnvironments } from "./environments"
|
import { initEnvironments } from "./environments"
|
||||||
import { initHistory } from "./history"
|
import { initHistory } from "./history"
|
||||||
@@ -24,7 +24,7 @@ export function initializeFirebase() {
|
|||||||
try {
|
try {
|
||||||
initializeApp(firebaseConfig)
|
initializeApp(firebaseConfig)
|
||||||
|
|
||||||
initAuth()
|
platform.auth.performAuthInit()
|
||||||
initSettings()
|
initSettings()
|
||||||
initCollections()
|
initCollections()
|
||||||
initHistory()
|
initHistory()
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore"
|
import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore"
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
import { HoppRESTRequest, translateToNewRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest, translateToNewRequest } from "@hoppscotch/data"
|
||||||
import { currentUser$, HoppUser } from "./auth"
|
import { platform } from "~/platform"
|
||||||
|
import { HoppUser } from "~/platform/auth"
|
||||||
import { restRequest$ } from "~/newstore/RESTSession"
|
import { restRequest$ } from "~/newstore/RESTSession"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +45,7 @@ function writeCurrentRequest(user: HoppUser, request: HoppRESTRequest) {
|
|||||||
* @returns Fetched request object if exists else null
|
* @returns Fetched request object if exists else null
|
||||||
*/
|
*/
|
||||||
export async function loadRequestFromSync(): Promise<HoppRESTRequest | null> {
|
export async function loadRequestFromSync(): Promise<HoppRESTRequest | null> {
|
||||||
const currentUser = currentUser$.value
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
if (!currentUser)
|
if (!currentUser)
|
||||||
throw new Error("Cannot load request from sync without login")
|
throw new Error("Cannot load request from sync without login")
|
||||||
@@ -66,6 +67,8 @@ export async function loadRequestFromSync(): Promise<HoppRESTRequest | null> {
|
|||||||
* Unsubscribe to stop syncing.
|
* Unsubscribe to stop syncing.
|
||||||
*/
|
*/
|
||||||
export function startRequestSync(): Subscription {
|
export function startRequestSync(): Subscription {
|
||||||
|
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||||
|
|
||||||
const sub = combineLatest([
|
const sub = combineLatest([
|
||||||
currentUser$,
|
currentUser$,
|
||||||
restRequest$.pipe(distinctUntilChanged()),
|
restRequest$.pipe(distinctUntilChanged()),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
onSnapshot,
|
onSnapshot,
|
||||||
setDoc,
|
setDoc,
|
||||||
} from "firebase/firestore"
|
} from "firebase/firestore"
|
||||||
import { currentUser$ } from "./auth"
|
import { platform } from "~/platform"
|
||||||
import { applySetting, settingsStore, SettingsType } from "~/newstore/settings"
|
import { applySetting, settingsStore, SettingsType } from "~/newstore/settings"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,21 +20,23 @@ let loadedSettings = false
|
|||||||
* Write Transform
|
* Write Transform
|
||||||
*/
|
*/
|
||||||
async function writeSettings(setting: string, value: any) {
|
async function writeSettings(setting: string, value: any) {
|
||||||
if (currentUser$.value === null)
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
|
if (currentUser === null)
|
||||||
throw new Error("Cannot write setting, user not signed in")
|
throw new Error("Cannot write setting, user not signed in")
|
||||||
|
|
||||||
const st = {
|
const st = {
|
||||||
updatedOn: new Date(),
|
updatedOn: new Date(),
|
||||||
author: currentUser$.value.uid,
|
author: currentUser.uid,
|
||||||
author_name: currentUser$.value.displayName,
|
author_name: currentUser.displayName,
|
||||||
author_image: currentUser$.value.photoURL,
|
author_image: currentUser.photoURL,
|
||||||
name: setting,
|
name: setting,
|
||||||
value,
|
value,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setDoc(
|
await setDoc(
|
||||||
doc(getFirestore(), "users", currentUser$.value.uid, "settings", setting),
|
doc(getFirestore(), "users", currentUser.uid, "settings", setting),
|
||||||
st
|
st
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -44,8 +46,11 @@ async function writeSettings(setting: string, value: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initSettings() {
|
export function initSettings() {
|
||||||
|
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||||
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
settingsStore.dispatches$.subscribe((dispatch) => {
|
settingsStore.dispatches$.subscribe((dispatch) => {
|
||||||
if (currentUser$.value && loadedSettings) {
|
if (currentUser && loadedSettings) {
|
||||||
if (dispatch.dispatcher === "bulkApplySettings") {
|
if (dispatch.dispatcher === "bulkApplySettings") {
|
||||||
Object.keys(dispatch.payload).forEach((key) => {
|
Object.keys(dispatch.payload).forEach((key) => {
|
||||||
writeSettings(key, dispatch.payload[key])
|
writeSettings(key, dispatch.payload[key])
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import * as E from "fp-ts/Either"
|
|
||||||
import { BehaviorSubject } from "rxjs"
|
|
||||||
import { authIdToken$ } from "../fb/auth"
|
|
||||||
import { runGQLQuery } from "../backend/GQLClient"
|
|
||||||
import { GetUserInfoDocument } from "../backend/graphql"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file deals with interfacing data provided by the
|
|
||||||
* Hoppscotch Backend server
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the information provided about a user
|
|
||||||
*/
|
|
||||||
export interface UserInfo {
|
|
||||||
/**
|
|
||||||
* UID of the user
|
|
||||||
*/
|
|
||||||
uid: string
|
|
||||||
/**
|
|
||||||
* Displayable name of the user (or null if none available)
|
|
||||||
*/
|
|
||||||
displayName: string | null
|
|
||||||
/**
|
|
||||||
* Email of the user (or null if none available)
|
|
||||||
*/
|
|
||||||
email: string | null
|
|
||||||
/**
|
|
||||||
* URL to the profile photo of the user (or null if none available)
|
|
||||||
*/
|
|
||||||
photoURL: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An observable subject onto the currently logged in user info (is null if not logged in)
|
|
||||||
*/
|
|
||||||
export const currentUserInfo$ = new BehaviorSubject<UserInfo | null>(null)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the currenUserInfo$ view and sets up its update mechanism
|
|
||||||
*/
|
|
||||||
export function initUserInfo() {
|
|
||||||
authIdToken$.subscribe((token) => {
|
|
||||||
if (token) {
|
|
||||||
updateUserInfo()
|
|
||||||
} else {
|
|
||||||
currentUserInfo$.next(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs the actual user info fetching
|
|
||||||
*/
|
|
||||||
async function updateUserInfo() {
|
|
||||||
const result = await runGQLQuery({
|
|
||||||
query: GetUserInfoDocument,
|
|
||||||
})
|
|
||||||
|
|
||||||
currentUserInfo$.next(
|
|
||||||
pipe(
|
|
||||||
result,
|
|
||||||
E.matchW(
|
|
||||||
() => null,
|
|
||||||
(x) => ({
|
|
||||||
uid: x.me.uid,
|
|
||||||
displayName: x.me.displayName ?? null,
|
|
||||||
email: x.me.email ?? null,
|
|
||||||
photoURL: x.me.photoURL ?? null,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import * as E from "fp-ts/Either"
|
|||||||
import { BehaviorSubject } from "rxjs"
|
import { BehaviorSubject } from "rxjs"
|
||||||
import { GQLError, runGQLQuery } from "../backend/GQLClient"
|
import { GQLError, runGQLQuery } from "../backend/GQLClient"
|
||||||
import { GetMyTeamsDocument, GetMyTeamsQuery } from "../backend/graphql"
|
import { GetMyTeamsDocument, GetMyTeamsQuery } from "../backend/graphql"
|
||||||
import { authIdToken$ } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
const BACKEND_PAGE_SIZE = 10
|
const BACKEND_PAGE_SIZE = 10
|
||||||
const POLL_DURATION = 10000
|
const POLL_DURATION = 10000
|
||||||
@@ -47,8 +47,10 @@ export default class TeamListAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchList() {
|
async fetchList() {
|
||||||
|
const currentUser = platform.auth.getCurrentUser()
|
||||||
|
|
||||||
// if the authIdToken is not present, don't fetch the teams list, as it will fail anyway
|
// if the authIdToken is not present, don't fetch the teams list, as it will fail anyway
|
||||||
if (!authIdToken$.value) return
|
if (!currentUser) return
|
||||||
|
|
||||||
this.loading$.next(true)
|
this.loading$.next(true)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createApp, Ref } from "vue"
|
import { createApp } from "vue"
|
||||||
|
import { PlatformDef, setPlatformDef } from "./platform"
|
||||||
import { setupLocalPersistence } from "./newstore/localpersistence"
|
import { setupLocalPersistence } from "./newstore/localpersistence"
|
||||||
import { performMigrations } from "./helpers/migrations"
|
import { performMigrations } from "./helpers/migrations"
|
||||||
import { initializeFirebase } from "./helpers/fb"
|
import { initializeFirebase } from "./helpers/fb"
|
||||||
import { initUserInfo } from "./helpers/teams/BackendUserInfo"
|
import { initBackendGQLClient } from "./helpers/backend/GQLClient"
|
||||||
import { HOPP_MODULES } from "@modules/."
|
import { HOPP_MODULES } from "@modules/."
|
||||||
|
|
||||||
import "virtual:windi.css"
|
import "virtual:windi.css"
|
||||||
@@ -12,33 +13,16 @@ import "nprogress/nprogress.css"
|
|||||||
|
|
||||||
import App from "./App.vue"
|
import App from "./App.vue"
|
||||||
|
|
||||||
export type PlatformDef = {
|
|
||||||
ui?: {
|
|
||||||
appHeader?: {
|
|
||||||
paddingTop?: Ref<string>
|
|
||||||
paddingLeft?: Ref<string>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the fields, functions and properties that will be
|
|
||||||
* filled in by the individual platforms.
|
|
||||||
*
|
|
||||||
* This value is populated upon calling `createHoppApp`
|
|
||||||
*/
|
|
||||||
export let platform: PlatformDef
|
|
||||||
|
|
||||||
export function createHoppApp(el: string | Element, platformDef: PlatformDef) {
|
export function createHoppApp(el: string | Element, platformDef: PlatformDef) {
|
||||||
platform = platformDef
|
setPlatformDef(platformDef)
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
// Some basic work that needs to be done before module inits even
|
// Some basic work that needs to be done before module inits even
|
||||||
initializeFirebase()
|
initializeFirebase()
|
||||||
|
initBackendGQLClient()
|
||||||
setupLocalPersistence()
|
setupLocalPersistence()
|
||||||
performMigrations()
|
performMigrations()
|
||||||
initUserInfo()
|
|
||||||
|
|
||||||
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app))
|
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app))
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { settingsStore } from "~/newstore/settings"
|
|||||||
import { App } from "vue"
|
import { App } from "vue"
|
||||||
import { APP_IS_IN_DEV_MODE } from "~/helpers/dev"
|
import { APP_IS_IN_DEV_MODE } from "~/helpers/dev"
|
||||||
import { gqlClientError$ } from "~/helpers/backend/GQLClient"
|
import { gqlClientError$ } from "~/helpers/backend/GQLClient"
|
||||||
import { currentUser$ } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tag names we allow giving to Sentry
|
* The tag names we allow giving to Sentry
|
||||||
@@ -164,6 +164,8 @@ function subscribeToAppEventsForReporting() {
|
|||||||
* additional data tags for the error reporting
|
* additional data tags for the error reporting
|
||||||
*/
|
*/
|
||||||
function subscribeForAppDataTags() {
|
function subscribeForAppDataTags() {
|
||||||
|
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||||
|
|
||||||
currentUser$.subscribe((user) => {
|
currentUser$.subscribe((user) => {
|
||||||
if (sentryActive) {
|
if (sentryActive) {
|
||||||
Sentry.setTag("user_logged_in", !!user)
|
Sentry.setTag("user_logged_in", !!user)
|
||||||
|
|||||||
@@ -10,8 +10,7 @@
|
|||||||
import { defineComponent } from "vue"
|
import { defineComponent } from "vue"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { initializeFirebase } from "~/helpers/fb"
|
import { initializeFirebase } from "~/helpers/fb"
|
||||||
import { isSignInWithEmailLink, signInWithEmailLink } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
import { getLocalConfig, removeLocalConfig } from "~/newstore/localpersistence"
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
@@ -29,29 +28,14 @@ export default defineComponent({
|
|||||||
initializeFirebase()
|
initializeFirebase()
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
if (isSignInWithEmailLink(window.location.href)) {
|
this.signingInWithEmail = true
|
||||||
this.signingInWithEmail = true
|
|
||||||
|
|
||||||
let email = getLocalConfig("emailForSignIn")
|
try {
|
||||||
|
await platform.auth.processMagicLink()
|
||||||
if (!email) {
|
} catch (e) {
|
||||||
email = window.prompt(
|
this.error = e.message
|
||||||
"Please provide your email for confirmation"
|
} finally {
|
||||||
) as string
|
this.signingInWithEmail = false
|
||||||
}
|
|
||||||
|
|
||||||
await signInWithEmailLink(email, window.location.href)
|
|
||||||
.then(() => {
|
|
||||||
removeLocalConfig("emailForSignIn")
|
|
||||||
this.$router.push({ path: "/" })
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
this.signingInWithEmail = false
|
|
||||||
this.error = e.message
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.signingInWithEmail = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ import {
|
|||||||
} from "~/helpers/backend/graphql"
|
} from "~/helpers/backend/graphql"
|
||||||
import { acceptTeamInvitation } from "~/helpers/backend/mutations/TeamInvitation"
|
import { acceptTeamInvitation } from "~/helpers/backend/mutations/TeamInvitation"
|
||||||
import { initializeFirebase } from "~/helpers/fb"
|
import { initializeFirebase } from "~/helpers/fb"
|
||||||
import { currentUser$, probableUser$ } from "~/helpers/fb/auth"
|
import { platform } from "~/platform"
|
||||||
import { onLoggedIn } from "@composables/auth"
|
import { onLoggedIn } from "@composables/auth"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
@@ -197,8 +197,15 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const probableUser = useReadonlyStream(probableUser$, null)
|
const probableUser = useReadonlyStream(
|
||||||
const currentUser = useReadonlyStream(currentUser$, null)
|
platform.auth.getProbableUserStream(),
|
||||||
|
platform.auth.getProbableUser()
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
const loadingCurrentUser = computed(() => {
|
const loadingCurrentUser = computed(() => {
|
||||||
if (!probableUser.value) return false
|
if (!probableUser.value) return false
|
||||||
|
|||||||
@@ -211,13 +211,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watchEffect, computed } from "vue"
|
import { ref, watchEffect, computed } from "vue"
|
||||||
import {
|
|
||||||
currentUser$,
|
import { platform } from "~/platform"
|
||||||
probableUser$,
|
|
||||||
setDisplayName,
|
|
||||||
setEmailAddress,
|
|
||||||
verifyEmailAddress,
|
|
||||||
} from "~/helpers/fb/auth"
|
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
@@ -247,8 +243,14 @@ usePageHead({
|
|||||||
const SYNC_COLLECTIONS = useSetting("syncCollections")
|
const SYNC_COLLECTIONS = useSetting("syncCollections")
|
||||||
const SYNC_ENVIRONMENTS = useSetting("syncEnvironments")
|
const SYNC_ENVIRONMENTS = useSetting("syncEnvironments")
|
||||||
const SYNC_HISTORY = useSetting("syncHistory")
|
const SYNC_HISTORY = useSetting("syncHistory")
|
||||||
const currentUser = useReadonlyStream(currentUser$, null)
|
const currentUser = useReadonlyStream(
|
||||||
const probableUser = useReadonlyStream(probableUser$, null)
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
const probableUser = useReadonlyStream(
|
||||||
|
platform.auth.getProbableUserStream(),
|
||||||
|
platform.auth.getProbableUser()
|
||||||
|
)
|
||||||
|
|
||||||
const loadingCurrentUser = computed(() => {
|
const loadingCurrentUser = computed(() => {
|
||||||
if (!probableUser.value) return false
|
if (!probableUser.value) return false
|
||||||
@@ -262,7 +264,8 @@ watchEffect(() => (displayName.value = currentUser.value?.displayName))
|
|||||||
|
|
||||||
const updateDisplayName = () => {
|
const updateDisplayName = () => {
|
||||||
updatingDisplayName.value = true
|
updatingDisplayName.value = true
|
||||||
setDisplayName(displayName.value as string)
|
platform.auth
|
||||||
|
.setDisplayName(displayName.value as string)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`${t("profile.updated")}`)
|
toast.success(`${t("profile.updated")}`)
|
||||||
})
|
})
|
||||||
@@ -280,7 +283,8 @@ watchEffect(() => (emailAddress.value = currentUser.value?.email))
|
|||||||
|
|
||||||
const updateEmailAddress = () => {
|
const updateEmailAddress = () => {
|
||||||
updatingEmailAddress.value = true
|
updatingEmailAddress.value = true
|
||||||
setEmailAddress(emailAddress.value as string)
|
platform.auth
|
||||||
|
.setEmailAddress(emailAddress.value as string)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`${t("profile.updated")}`)
|
toast.success(`${t("profile.updated")}`)
|
||||||
})
|
})
|
||||||
@@ -296,7 +300,8 @@ const verifyingEmailAddress = ref(false)
|
|||||||
|
|
||||||
const sendEmailVerification = () => {
|
const sendEmailVerification = () => {
|
||||||
verifyingEmailAddress.value = true
|
verifyingEmailAddress.value = true
|
||||||
verifyEmailAddress()
|
platform.auth
|
||||||
|
.verifyEmailAddress()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`${t("profile.email_verification_mail")}`)
|
toast.success(`${t("profile.email_verification_mail")}`)
|
||||||
})
|
})
|
||||||
|
|||||||
214
packages/hoppscotch-common/src/platform/auth.ts
Normal file
214
packages/hoppscotch-common/src/platform/auth.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import { ClientOptions } from "@urql/core"
|
||||||
|
import { Observable } from "rxjs"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common (and required) set of fields that describe a user.
|
||||||
|
*/
|
||||||
|
export type HoppUser = {
|
||||||
|
/** A unique ID identifying the user */
|
||||||
|
uid: string
|
||||||
|
|
||||||
|
/** The name to be displayed as the user's */
|
||||||
|
displayName: string | null
|
||||||
|
|
||||||
|
/** The user's email address */
|
||||||
|
email: string | null
|
||||||
|
|
||||||
|
/** URL to the profile picture of the user */
|
||||||
|
photoURL: string | null
|
||||||
|
|
||||||
|
// Regarding `provider` and `accessToken`:
|
||||||
|
// The current implementation and use case for these 2 fields are super weird due to legacy.
|
||||||
|
// Currrently these fields are only basically populated for Github Auth as we need the access token issued
|
||||||
|
// by it to implement Gist submission. I would really love refactor to make this thing more sane.
|
||||||
|
|
||||||
|
/** Name of the provider authenticating (NOTE: See notes on `platform/auth.ts`) */
|
||||||
|
provider?: string
|
||||||
|
/** Access Token for the auth of the user against the given `provider`. */
|
||||||
|
accessToken?: string
|
||||||
|
emailVerified: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthEvent =
|
||||||
|
| { event: "probable_login"; user: HoppUser } // We have previous login state, but the app is waiting for authentication
|
||||||
|
| { event: "login"; user: HoppUser } // We are authenticated
|
||||||
|
| { event: "logout" } // No authentication and we have no previous state
|
||||||
|
|
||||||
|
export type GithubSignInResult =
|
||||||
|
| { type: "success"; user: HoppUser } // The authentication was a success
|
||||||
|
| { type: "account-exists-with-different-cred"; link: () => Promise<void> } // We authenticated correctly, but the provider didn't match, so we give the user the opportunity to link to continue completing auth
|
||||||
|
| { type: "error"; err: unknown } // Auth failed completely and we don't know why
|
||||||
|
|
||||||
|
export type AuthPlatformDef = {
|
||||||
|
/**
|
||||||
|
* Returns an observable that emits the current user as per the auth implementation.
|
||||||
|
*
|
||||||
|
* NOTES:
|
||||||
|
* 1. Make sure to emit non-null values once you have credentials to perform backend operations. (Get required tokens ?)
|
||||||
|
* 2. It is best to let the stream emit a value immediately on subscription (we can do that by basing this on a BehaviourSubject)
|
||||||
|
*
|
||||||
|
* @returns An observable which returns a `HoppUser` or null if not logged in (or login not completed)
|
||||||
|
*/
|
||||||
|
getCurrentUserStream: () => Observable<HoppUser | null>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a stream to events happening in the auth mechanism. Common uses these events to
|
||||||
|
* let subsystems know something is changed by the authentication status and to react accordingly
|
||||||
|
*
|
||||||
|
* @returns An observable which emits an AuthEvent over time
|
||||||
|
*/
|
||||||
|
getAuthEventsStream: () => Observable<AuthEvent>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to `getCurrentUserStream` but deals with the authentication being `probable`.
|
||||||
|
* Probable User for states where, "We haven't authed yet but we are guessing this person will auth eventually".
|
||||||
|
* This allows for things like Header component to presumpt a state until we auth properly and avoid flashing a "logged out" state.
|
||||||
|
*
|
||||||
|
* NOTES:
|
||||||
|
* 1. It is best to let the stream emit a value immediately on subscription (we can do that by basing this on a BehaviourSubject)
|
||||||
|
* 2. Once the authentication is confirmed, this stream should emit the same values as `getCurrentUserStream`.
|
||||||
|
*
|
||||||
|
* @returns An obsverable which returns a `HoppUser` for the probable user (or confirmed user if authed) or null if we don't know about a probable user
|
||||||
|
*/
|
||||||
|
getProbableUserStream: () => Observable<HoppUser | null>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently authed user. (Similar rules apply as `getCurrentUserStream`)
|
||||||
|
* @returns The authenticated user or null if not logged in
|
||||||
|
*/
|
||||||
|
getCurrentUser: () => HoppUser | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the most probable to complete auth user. (Similar rules apply as `getProbableUserStream`)
|
||||||
|
* @returns The probable user or null if have no idea who will auth in
|
||||||
|
*/
|
||||||
|
getProbableUser: () => HoppUser | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [This is only for Common Init logic to call!]
|
||||||
|
* Called by Common when it is time to perform initialization activities for authentication.
|
||||||
|
* (This is the best place to do init work for the auth subsystem in the platform).
|
||||||
|
*/
|
||||||
|
performAuthInit: () => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the headers that should be applied by the backend GQL API client (see GQLClient)
|
||||||
|
* inorder to talk to the backend (like apply auth headers ?)
|
||||||
|
* @returns An object with the header key and header values as strings
|
||||||
|
*/
|
||||||
|
getBackendHeaders: () => Record<string, string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the backend GQL API client encounters an auth error to check if with the
|
||||||
|
* current state, if an auth error is possible. This lets the backend GQL client know that
|
||||||
|
* it can expect an auth error and we should wait and (possibly retry) to re-execute an operation.
|
||||||
|
* This is useful for cases where queries might fail as the tokens just expired and need to be refreshed,
|
||||||
|
* so the app can get the new token and GQL client knows to re-execute the same query.
|
||||||
|
|
||||||
|
* @returns Whether an error is expected or not
|
||||||
|
*/
|
||||||
|
willBackendHaveAuthError: () => boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to register a callback where the backend GQL client should reconnect/reconfigure subscriptions
|
||||||
|
* as some communication parameter changed over time. Like for example, the backend subscription system
|
||||||
|
* on a id token based mechanism should be let known that the id token has changed and reconnect the subscription
|
||||||
|
* connection with the updated params.
|
||||||
|
* @param func The callback function to call
|
||||||
|
*/
|
||||||
|
onBackendGQLClientShouldReconnect: (func: () => void) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* provide the client options for GqlClient
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getGQLClientOptions?: () => ClientOptions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the string content that should be returned when the user selects to
|
||||||
|
* copy auth token from Developer Options.
|
||||||
|
*
|
||||||
|
* @returns The auth token (or equivalent) as a string if we have one to give, else null
|
||||||
|
*/
|
||||||
|
getDevOptsBackendIDToken: () => string | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an empty promise that only resolves when the current probable user because confirmed.
|
||||||
|
*
|
||||||
|
* Note:
|
||||||
|
* 1. Make sure there is a probable user before waiting, as if not, this function will throw
|
||||||
|
* 2. If the probable user is already confirmed, this function will return an immediately resolved promise
|
||||||
|
*/
|
||||||
|
waitProbableLoginToConfirm: () => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to sign in user with email (magic link). This should send backend calls to send the auth email.
|
||||||
|
* @param email The email that is logging in.
|
||||||
|
* @returns An empty promise that is resolved when the operation is complete
|
||||||
|
*/
|
||||||
|
signInWithEmail: (email: string) => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a given link is a valid sign in with email, magic link response url.
|
||||||
|
* (i.e, a URL that COULD be from a magic link email)
|
||||||
|
* @param url The url to check
|
||||||
|
* @returns Whether this is valid or not (NOTE: This is just a structural check not whether this is accepted (hence, not async))
|
||||||
|
*/
|
||||||
|
isSignInWithEmailLink: (url: string) => boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that validates the magic link redirect and signs in the user
|
||||||
|
*
|
||||||
|
* @param email - Email to log in to
|
||||||
|
* @param url - The action URL which is used to validate login
|
||||||
|
* @returns A promise that resolves with the user info when auth is completed
|
||||||
|
*/
|
||||||
|
signInWithEmailLink: (email: string, url: string) => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* function that validates the magic link & signs the user in
|
||||||
|
*/
|
||||||
|
processMagicLink: () => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends email verification email (the checkmark besides the email)
|
||||||
|
* @returns When the check has succeed and completed
|
||||||
|
*/
|
||||||
|
verifyEmailAddress: () => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs user in with Google.
|
||||||
|
* @returns A promise that resolves with the user info when auth is completed
|
||||||
|
*/
|
||||||
|
signInUserWithGoogle: () => Promise<void>
|
||||||
|
/**
|
||||||
|
* Signs user in with Github.
|
||||||
|
* @returns A promise that resolves with the auth status, giving an opportunity to link if or handle failures
|
||||||
|
*/
|
||||||
|
signInUserWithGithub: () => Promise<GithubSignInResult> | Promise<undefined>
|
||||||
|
/**
|
||||||
|
* Signs user in with Microsoft.
|
||||||
|
* @returns A promise that resolves with the user info when auth is completed
|
||||||
|
*/
|
||||||
|
signInUserWithMicrosoft: () => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs out the user from auth
|
||||||
|
* @returns An empty promise that is resolved when the operation is complete
|
||||||
|
*/
|
||||||
|
signOutUser: () => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the email address of the user
|
||||||
|
* @param email The new email to set this to.
|
||||||
|
* @returns An empty promise that is resolved when the operation is complete
|
||||||
|
*/
|
||||||
|
setEmailAddress: (email: string) => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the display name of the user
|
||||||
|
* @param name The new name to set this to.
|
||||||
|
* @returns An empty promise that is resolved when the operation is complete
|
||||||
|
*/
|
||||||
|
setDisplayName: (name: string) => Promise<void>
|
||||||
|
}
|
||||||
13
packages/hoppscotch-common/src/platform/index.ts
Normal file
13
packages/hoppscotch-common/src/platform/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { AuthPlatformDef } from "./auth"
|
||||||
|
import { UIPlatformDef } from "./ui"
|
||||||
|
|
||||||
|
export type PlatformDef = {
|
||||||
|
ui?: UIPlatformDef
|
||||||
|
auth: AuthPlatformDef
|
||||||
|
}
|
||||||
|
|
||||||
|
export let platform: PlatformDef
|
||||||
|
|
||||||
|
export function setPlatformDef(def: PlatformDef) {
|
||||||
|
platform = def
|
||||||
|
}
|
||||||
8
packages/hoppscotch-common/src/platform/ui.ts
Normal file
8
packages/hoppscotch-common/src/platform/ui.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Ref } from "vue"
|
||||||
|
|
||||||
|
export type UIPlatformDef = {
|
||||||
|
appHeader?: {
|
||||||
|
paddingTop?: Ref<string>
|
||||||
|
paddingLeft?: Ref<string>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,16 @@ import { HstVue } from "@histoire/plugin-vue"
|
|||||||
import { defineConfig } from "histoire"
|
import { defineConfig } from "histoire"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
theme: {
|
||||||
|
title: "Hoppscotch • UI",
|
||||||
|
logo: {
|
||||||
|
square: "/logo.png",
|
||||||
|
light: "/logo.png",
|
||||||
|
dark: "/logo.png",
|
||||||
|
},
|
||||||
|
// logoHref: "https://ui.hoppscotch.io",
|
||||||
|
favicon: 'favicon.ico',
|
||||||
|
},
|
||||||
setupFile: "histoire.setup.ts",
|
setupFile: "histoire.setup.ts",
|
||||||
plugins: [HstVue()],
|
plugins: [HstVue()],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"story:dev": "histoire dev",
|
"story:dev": "histoire dev",
|
||||||
"story:build": "histoire build",
|
"story:build": "histoire build",
|
||||||
"story:preview": "histoire preview"
|
"story:preview": "histoire preview",
|
||||||
|
"do-build-ui": "pnpm run story:build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||||
|
|||||||
BIN
packages/hoppscotch-ui/public/favicon.ico
Normal file
BIN
packages/hoppscotch-ui/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
packages/hoppscotch-ui/public/logo.png
Normal file
BIN
packages/hoppscotch-ui/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
37
packages/hoppscotch-ui/src/components.d.ts
vendored
Normal file
37
packages/hoppscotch-ui/src/components.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// generated by unplugin-vue-components
|
||||||
|
// We suggest you to commit this file into source control
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
import '@vue/runtime-core'
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare module '@vue/runtime-core' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
ButtonPrimary: typeof import('./components/button/Primary.vue')['default']
|
||||||
|
ButtonSecondary: typeof import('./components/button/Secondary.vue')['default']
|
||||||
|
IconLucideLoader: typeof import('~icons/lucide/loader')['default']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
SmartAnchor: typeof import('./components/smart/Anchor.vue')['default']
|
||||||
|
SmartAutoComplete: typeof import('./components/smart/AutoComplete.vue')['default']
|
||||||
|
SmartCheckbox: typeof import('./components/smart/Checkbox.vue')['default']
|
||||||
|
SmartConfirmModal: typeof import('./components/smart/ConfirmModal.vue')['default']
|
||||||
|
SmartExpand: typeof import('./components/smart/Expand.vue')['default']
|
||||||
|
SmartFileChip: typeof import('./components/smart/FileChip.vue')['default']
|
||||||
|
SmartIntersection: typeof import('./components/smart/Intersection.vue')['default']
|
||||||
|
SmartItem: typeof import('./components/smart/Item.vue')['default']
|
||||||
|
SmartLink: typeof import('./components/smart/Link.vue')['default']
|
||||||
|
SmartModal: typeof import('./components/smart/Modal.vue')['default']
|
||||||
|
SmartProgressRing: typeof import('./components/smart/ProgressRing.vue')['default']
|
||||||
|
SmartRadio: typeof import('./components/smart/Radio.vue')['default']
|
||||||
|
SmartRadioGroup: typeof import('./components/smart/RadioGroup.vue')['default']
|
||||||
|
SmartSlideOver: typeof import('./components/smart/SlideOver.vue')['default']
|
||||||
|
SmartSpinner: typeof import('./components/smart/Spinner.vue')['default']
|
||||||
|
SmartTab: typeof import('./components/smart/Tab.vue')['default']
|
||||||
|
SmartTabs: typeof import('./components/smart/Tabs.vue')['default']
|
||||||
|
SmartToggle: typeof import('./components/smart/Toggle.vue')['default']
|
||||||
|
SmartWindow: typeof import('./components/smart/Window.vue')['default']
|
||||||
|
SmartWindows: typeof import('./components/smart/Windows.vue')['default']
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Hoppscotch - Open source API development ecosystem</title>
|
<title>Hoppscotch • Open source API development ecosystem</title>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hoppscotch/common": "workspace:^",
|
"@hoppscotch/common": "workspace:^",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"firebase": "^9.8.4",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
|
"rxjs": "^7.5.5",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"util": "^0.12.4",
|
"util": "^0.12.4",
|
||||||
"vue": "^3.2.41",
|
"vue": "^3.2.41",
|
||||||
|
|||||||
436
packages/hoppscotch-web/src/firebase/auth.ts
Normal file
436
packages/hoppscotch-web/src/firebase/auth.ts
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
import {
|
||||||
|
AuthEvent,
|
||||||
|
AuthPlatformDef,
|
||||||
|
HoppUser,
|
||||||
|
} from "@hoppscotch/common/platform/auth"
|
||||||
|
import {
|
||||||
|
Subscription,
|
||||||
|
BehaviorSubject,
|
||||||
|
Subject,
|
||||||
|
filter,
|
||||||
|
map,
|
||||||
|
combineLatest,
|
||||||
|
} from "rxjs"
|
||||||
|
import {
|
||||||
|
setDoc,
|
||||||
|
onSnapshot,
|
||||||
|
updateDoc,
|
||||||
|
doc,
|
||||||
|
getFirestore,
|
||||||
|
} from "firebase/firestore"
|
||||||
|
import {
|
||||||
|
AuthError,
|
||||||
|
AuthCredential,
|
||||||
|
User as FBUser,
|
||||||
|
sendSignInLinkToEmail,
|
||||||
|
linkWithCredential,
|
||||||
|
getAuth,
|
||||||
|
ActionCodeSettings,
|
||||||
|
isSignInWithEmailLink as isSignInWithEmailLinkFB,
|
||||||
|
signInWithEmailLink as signInWithEmailLinkFB,
|
||||||
|
sendEmailVerification,
|
||||||
|
signInWithPopup,
|
||||||
|
GoogleAuthProvider,
|
||||||
|
GithubAuthProvider,
|
||||||
|
OAuthProvider,
|
||||||
|
fetchSignInMethodsForEmail,
|
||||||
|
updateEmail,
|
||||||
|
updateProfile,
|
||||||
|
reauthenticateWithCredential,
|
||||||
|
onAuthStateChanged,
|
||||||
|
onIdTokenChanged,
|
||||||
|
signOut,
|
||||||
|
} from "firebase/auth"
|
||||||
|
import {
|
||||||
|
getLocalConfig,
|
||||||
|
removeLocalConfig,
|
||||||
|
setLocalConfig,
|
||||||
|
} from "@hoppscotch/common/newstore/localpersistence"
|
||||||
|
|
||||||
|
export const currentUserFB$ = new BehaviorSubject<FBUser | null>(null)
|
||||||
|
export const authEvents$ = new Subject<AuthEvent>()
|
||||||
|
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||||
|
|
||||||
|
const authIdToken$ = new BehaviorSubject<string | null>(null)
|
||||||
|
|
||||||
|
async function signInWithEmailLink(email: string, url: string) {
|
||||||
|
return await signInWithEmailLinkFB(getAuth(), email, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fbUserToHoppUser(user: FBUser): HoppUser {
|
||||||
|
return {
|
||||||
|
uid: user.uid,
|
||||||
|
displayName: user.displayName,
|
||||||
|
email: user.email,
|
||||||
|
photoURL: user.photoURL,
|
||||||
|
emailVerified: user.emailVerified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||||
|
|
||||||
|
const EMAIL_ACTION_CODE_SETTINGS: ActionCodeSettings = {
|
||||||
|
url: `${import.meta.env.VITE_BASE_URL}/enter`,
|
||||||
|
handleCodeInApp: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signInUserWithGithubFB() {
|
||||||
|
return await signInWithPopup(
|
||||||
|
getAuth(),
|
||||||
|
new GithubAuthProvider().addScope("gist")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signInUserWithGoogleFB() {
|
||||||
|
return await signInWithPopup(getAuth(), new GoogleAuthProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signInUserWithMicrosoftFB() {
|
||||||
|
return await signInWithPopup(getAuth(), new OAuthProvider("microsoft.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reauthenticate the user with the given credential
|
||||||
|
*/
|
||||||
|
async function reauthenticateUser() {
|
||||||
|
if (!currentUserFB$.value || !currentUser$.value)
|
||||||
|
throw new Error("No user has logged in")
|
||||||
|
|
||||||
|
const currentAuthMethod = currentUser$.value.provider
|
||||||
|
|
||||||
|
let credential
|
||||||
|
if (currentAuthMethod === "google.com") {
|
||||||
|
// const result = await signInUserWithGithubFB()
|
||||||
|
const result = await signInUserWithGoogleFB()
|
||||||
|
credential = GithubAuthProvider.credentialFromResult(result)
|
||||||
|
} else if (currentAuthMethod === "github.com") {
|
||||||
|
// const result = await signInUserWithGoogleFB()
|
||||||
|
const result = await signInUserWithGithubFB()
|
||||||
|
credential = GoogleAuthProvider.credentialFromResult(result)
|
||||||
|
} else if (currentAuthMethod === "microsoft.com") {
|
||||||
|
const result = await signInUserWithMicrosoftFB()
|
||||||
|
credential = OAuthProvider.credentialFromResult(result)
|
||||||
|
} else if (currentAuthMethod === "password") {
|
||||||
|
const email = prompt(
|
||||||
|
"Reauthenticate your account using your current email:"
|
||||||
|
)
|
||||||
|
|
||||||
|
await def
|
||||||
|
.signInWithEmail(email as string)
|
||||||
|
.then(() =>
|
||||||
|
alert(
|
||||||
|
`Check your inbox - we sent an email to ${email}. It contains a magic link that will reauthenticate your account.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.catch((e) => {
|
||||||
|
alert(`Error: ${e.message}`)
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await reauthenticateWithCredential(
|
||||||
|
currentUserFB$.value,
|
||||||
|
credential as AuthCredential
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("error updating", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Links account with another account given in a auth/account-exists-with-different-credential error
|
||||||
|
*
|
||||||
|
* @param error - Error caught after trying to login
|
||||||
|
*
|
||||||
|
* @returns Promise of UserCredential
|
||||||
|
*/
|
||||||
|
async function linkWithFBCredentialFromAuthError(error: unknown) {
|
||||||
|
// credential is not null since this function is called after an auth/account-exists-with-different-credential error, ie credentials actually exist
|
||||||
|
const credentials = OAuthProvider.credentialFromError(error as AuthError)!
|
||||||
|
|
||||||
|
const otherLinkedProviders = (
|
||||||
|
await getSignInMethodsForEmail((error as AuthError).customData.email!)
|
||||||
|
).filter((providerId) => credentials.providerId !== providerId)
|
||||||
|
|
||||||
|
let user: FBUser | null = null
|
||||||
|
|
||||||
|
if (otherLinkedProviders.indexOf("google.com") >= -1) {
|
||||||
|
user = (await signInUserWithGoogleFB()).user
|
||||||
|
} else if (otherLinkedProviders.indexOf("github.com") >= -1) {
|
||||||
|
user = (await signInUserWithGithubFB()).user
|
||||||
|
} else if (otherLinkedProviders.indexOf("microsoft.com") >= -1) {
|
||||||
|
user = (await signInUserWithMicrosoftFB()).user
|
||||||
|
}
|
||||||
|
|
||||||
|
// user is not null since going through each provider will return a user
|
||||||
|
return await linkWithCredential(user!, credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setProviderInfo(id: string, token: string) {
|
||||||
|
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||||
|
|
||||||
|
const us = {
|
||||||
|
updatedOn: new Date(),
|
||||||
|
provider: id,
|
||||||
|
accessToken: token,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateDoc(doc(getFirestore(), "users", currentUser$.value.uid), us)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("error updating provider info", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSignInMethodsForEmail(email: string) {
|
||||||
|
return await fetchSignInMethodsForEmail(getAuth(), email)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const def: AuthPlatformDef = {
|
||||||
|
getCurrentUserStream: () => currentUser$,
|
||||||
|
getAuthEventsStream: () => authEvents$,
|
||||||
|
getProbableUserStream: () => probableUser$,
|
||||||
|
|
||||||
|
getCurrentUser: () => currentUser$.value,
|
||||||
|
getProbableUser: () => probableUser$.value,
|
||||||
|
|
||||||
|
getBackendHeaders() {
|
||||||
|
return {
|
||||||
|
authorization: `Bearer ${authIdToken$.value}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
willBackendHaveAuthError() {
|
||||||
|
return !authIdToken$.value
|
||||||
|
},
|
||||||
|
onBackendGQLClientShouldReconnect(func) {
|
||||||
|
authIdToken$.subscribe(() => {
|
||||||
|
func()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getDevOptsBackendIDToken() {
|
||||||
|
return authIdToken$.value
|
||||||
|
},
|
||||||
|
performAuthInit() {
|
||||||
|
// todo: implement
|
||||||
|
const auth = getAuth()
|
||||||
|
const firestore = getFirestore()
|
||||||
|
|
||||||
|
combineLatest([currentUserFB$, authIdToken$])
|
||||||
|
.pipe(
|
||||||
|
map(([user, token]) => {
|
||||||
|
// If there is no auth token, we will just consider as the auth as not complete
|
||||||
|
if (token === null) return null
|
||||||
|
if (user !== null) return fbUserToHoppUser(user)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((x) => {
|
||||||
|
currentUser$.next(x)
|
||||||
|
})
|
||||||
|
|
||||||
|
let extraSnapshotStop: (() => void) | null = null
|
||||||
|
|
||||||
|
probableUser$.next(JSON.parse(getLocalConfig("login_state") ?? "null"))
|
||||||
|
|
||||||
|
onAuthStateChanged(auth, (user) => {
|
||||||
|
const wasLoggedIn = currentUser$.value !== null
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
probableUser$.next(user)
|
||||||
|
} else {
|
||||||
|
probableUser$.next(null)
|
||||||
|
removeLocalConfig("login_state")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user && extraSnapshotStop) {
|
||||||
|
extraSnapshotStop()
|
||||||
|
extraSnapshotStop = null
|
||||||
|
} else if (user) {
|
||||||
|
// Merge all the user info from all the authenticated providers
|
||||||
|
user.providerData.forEach((profile) => {
|
||||||
|
if (!profile) return
|
||||||
|
|
||||||
|
const us = {
|
||||||
|
updatedOn: new Date(),
|
||||||
|
provider: profile.providerId,
|
||||||
|
name: profile.displayName,
|
||||||
|
email: profile.email,
|
||||||
|
photoUrl: profile.photoURL,
|
||||||
|
uid: profile.uid,
|
||||||
|
}
|
||||||
|
|
||||||
|
setDoc(doc(firestore, "users", user.uid), us, { merge: true }).catch(
|
||||||
|
(e) => console.error("error updating", us, e)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
extraSnapshotStop = onSnapshot(
|
||||||
|
doc(firestore, "users", user.uid),
|
||||||
|
(doc) => {
|
||||||
|
const data = doc.data()
|
||||||
|
|
||||||
|
const userUpdate: HoppUser = fbUserToHoppUser(user)
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
// Write extra provider data
|
||||||
|
userUpdate.provider = data.provider
|
||||||
|
userUpdate.accessToken = data.accessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser$.next(userUpdate)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUserFB$.next(user)
|
||||||
|
currentUser$.next(user === null ? null : fbUserToHoppUser(user))
|
||||||
|
|
||||||
|
// User wasn't found before, but now is there (login happened)
|
||||||
|
if (!wasLoggedIn && user) {
|
||||||
|
authEvents$.next({
|
||||||
|
event: "login",
|
||||||
|
user: currentUser$.value!,
|
||||||
|
})
|
||||||
|
} else if (wasLoggedIn && !user) {
|
||||||
|
// User was found before, but now is not there (logout happened)
|
||||||
|
authEvents$.next({
|
||||||
|
event: "logout",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onIdTokenChanged(auth, async (user) => {
|
||||||
|
if (user) {
|
||||||
|
authIdToken$.next(await user.getIdToken())
|
||||||
|
|
||||||
|
setLocalConfig("login_state", JSON.stringify(user))
|
||||||
|
} else {
|
||||||
|
authIdToken$.next(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
waitProbableLoginToConfirm() {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if (authIdToken$.value) resolve()
|
||||||
|
|
||||||
|
if (!probableUser$.value) reject(new Error("no_probable_user"))
|
||||||
|
|
||||||
|
let sub: Subscription | null = null
|
||||||
|
sub = authIdToken$.pipe(filter((token) => !!token)).subscribe(() => {
|
||||||
|
sub?.unsubscribe()
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async signInWithEmail(email: string) {
|
||||||
|
return await sendSignInLinkToEmail(
|
||||||
|
getAuth(),
|
||||||
|
email,
|
||||||
|
EMAIL_ACTION_CODE_SETTINGS
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
isSignInWithEmailLink(url: string) {
|
||||||
|
return isSignInWithEmailLinkFB(getAuth(), url)
|
||||||
|
},
|
||||||
|
|
||||||
|
async verifyEmailAddress() {
|
||||||
|
if (!currentUserFB$.value) throw new Error("No user has logged in")
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendEmailVerification(currentUserFB$.value)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("error verifying email address", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async signInUserWithGoogle() {
|
||||||
|
await signInUserWithGoogleFB()
|
||||||
|
},
|
||||||
|
async signInUserWithGithub() {
|
||||||
|
try {
|
||||||
|
const cred = await signInUserWithGithubFB()
|
||||||
|
const oAuthCred = GithubAuthProvider.credentialFromResult(cred)!
|
||||||
|
const token = oAuthCred.accessToken
|
||||||
|
await setProviderInfo(cred.providerId!, token!)
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "success",
|
||||||
|
user: fbUserToHoppUser(cred.user),
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("error while logging in with github", e)
|
||||||
|
|
||||||
|
if ((e as any).code === "auth/account-exists-with-different-credential") {
|
||||||
|
return {
|
||||||
|
type: "account-exists-with-different-cred",
|
||||||
|
link: async () => {
|
||||||
|
await linkWithFBCredentialFromAuthError(e)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: "error",
|
||||||
|
err: e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async signInUserWithMicrosoft() {
|
||||||
|
await signInUserWithMicrosoftFB()
|
||||||
|
},
|
||||||
|
async signInWithEmailLink(email: string, url: string) {
|
||||||
|
await signInWithEmailLinkFB(getAuth(), email, url)
|
||||||
|
},
|
||||||
|
async setEmailAddress(email: string) {
|
||||||
|
if (!currentUserFB$.value) throw new Error("No user has logged in")
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateEmail(currentUserFB$.value, email)
|
||||||
|
} catch (e) {
|
||||||
|
await reauthenticateUser()
|
||||||
|
console.log("error setting email address", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setDisplayName(name: string) {
|
||||||
|
if (!currentUserFB$.value) throw new Error("No user has logged in")
|
||||||
|
|
||||||
|
const us = {
|
||||||
|
displayName: name,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateProfile(currentUserFB$.value, us)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("error updating display name", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async signOutUser() {
|
||||||
|
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||||
|
|
||||||
|
await signOut(getAuth())
|
||||||
|
},
|
||||||
|
async processMagicLink() {
|
||||||
|
if (this.isSignInWithEmailLink(window.location.href)) {
|
||||||
|
let email = getLocalConfig("emailForSignIn")
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
email = window.prompt(
|
||||||
|
"Please provide your email for confirmation"
|
||||||
|
) as string
|
||||||
|
}
|
||||||
|
|
||||||
|
await signInWithEmailLink(email, window.location.href)
|
||||||
|
|
||||||
|
removeLocalConfig("emailForSignIn")
|
||||||
|
window.location.href = "/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
import { createHoppApp } from "@hoppscotch/common"
|
import { createHoppApp } from "@hoppscotch/common"
|
||||||
|
import { def as authDef } from "./firebase/auth"
|
||||||
|
|
||||||
createHoppApp("#app", {})
|
createHoppApp("#app", {
|
||||||
|
auth: authDef,
|
||||||
|
})
|
||||||
|
|||||||
47
pnpm-lock.yaml
generated
47
pnpm-lock.yaml
generated
@@ -345,7 +345,7 @@ importers:
|
|||||||
vite-plugin-inspect: 0.7.4_vite@3.1.4
|
vite-plugin-inspect: 0.7.4_vite@3.1.4
|
||||||
vite-plugin-pages: 0.26.0_vnheu5mvzzbfbuhqo4shkhdhei
|
vite-plugin-pages: 0.26.0_vnheu5mvzzbfbuhqo4shkhdhei
|
||||||
vite-plugin-pages-sitemap: 1.4.0
|
vite-plugin-pages-sitemap: 1.4.0
|
||||||
vite-plugin-pwa: 0.13.1_bg4cnt4dy3xq3a47wkujd6ryzq
|
vite-plugin-pwa: 0.13.1_vite@3.1.4
|
||||||
vite-plugin-vue-layouts: 0.7.0_oewzdqozxqnqgsrjzmwikx34vi
|
vite-plugin-vue-layouts: 0.7.0_oewzdqozxqnqgsrjzmwikx34vi
|
||||||
vite-plugin-windicss: 1.8.8_vite@3.1.4
|
vite-plugin-windicss: 1.8.8_vite@3.1.4
|
||||||
vue-tsc: 0.38.2_typescript@4.7.4
|
vue-tsc: 0.38.2_typescript@4.7.4
|
||||||
@@ -561,7 +561,9 @@ importers:
|
|||||||
eslint: ^8.28.0
|
eslint: ^8.28.0
|
||||||
eslint-plugin-prettier: ^4.2.1
|
eslint-plugin-prettier: ^4.2.1
|
||||||
eslint-plugin-vue: ^9.5.1
|
eslint-plugin-vue: ^9.5.1
|
||||||
|
firebase: ^9.8.4
|
||||||
process: ^0.11.10
|
process: ^0.11.10
|
||||||
|
rxjs: ^7.5.5
|
||||||
stream-browserify: ^3.0.0
|
stream-browserify: ^3.0.0
|
||||||
typescript: ^4.6.4
|
typescript: ^4.6.4
|
||||||
unplugin-icons: ^0.14.9
|
unplugin-icons: ^0.14.9
|
||||||
@@ -584,7 +586,9 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@hoppscotch/common': link:../hoppscotch-common
|
'@hoppscotch/common': link:../hoppscotch-common
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
|
firebase: 9.8.4
|
||||||
process: 0.11.10
|
process: 0.11.10
|
||||||
|
rxjs: 7.5.5
|
||||||
stream-browserify: 3.0.0
|
stream-browserify: 3.0.0
|
||||||
util: 0.12.4
|
util: 0.12.4
|
||||||
vue: 3.2.45
|
vue: 3.2.45
|
||||||
@@ -610,7 +614,7 @@ importers:
|
|||||||
vite-plugin-inspect: 0.7.4_vite@3.2.4
|
vite-plugin-inspect: 0.7.4_vite@3.2.4
|
||||||
vite-plugin-pages: 0.26.0_vite@3.2.4
|
vite-plugin-pages: 0.26.0_vite@3.2.4
|
||||||
vite-plugin-pages-sitemap: 1.4.0
|
vite-plugin-pages-sitemap: 1.4.0
|
||||||
vite-plugin-pwa: 0.13.1_3kw35epztoiwny7qtfesjexvtu
|
vite-plugin-pwa: 0.13.1_vite@3.2.4
|
||||||
vite-plugin-static-copy: 0.12.0_vite@3.2.4
|
vite-plugin-static-copy: 0.12.0_vite@3.2.4
|
||||||
vite-plugin-vue-layouts: 0.7.0_vite@3.2.4+vue@3.2.45
|
vite-plugin-vue-layouts: 0.7.0_vite@3.2.4+vue@3.2.45
|
||||||
vite-plugin-windicss: 1.8.8_vite@3.2.4
|
vite-plugin-windicss: 1.8.8_vite@3.2.4
|
||||||
@@ -5566,7 +5570,7 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/after/0.8.2:
|
/after/0.8.2:
|
||||||
resolution: {integrity: sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=}
|
resolution: {integrity: sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/agent-base/6.0.2:
|
/agent-base/6.0.2:
|
||||||
@@ -5923,7 +5927,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
/base64-arraybuffer/0.1.4:
|
/base64-arraybuffer/0.1.4:
|
||||||
resolution: {integrity: sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=}
|
resolution: {integrity: sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==}
|
||||||
engines: {node: '>= 0.6.0'}
|
engines: {node: '>= 0.6.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -6369,7 +6373,7 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/component-bind/1.0.0:
|
/component-bind/1.0.0:
|
||||||
resolution: {integrity: sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=}
|
resolution: {integrity: sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/component-emitter/1.3.0:
|
/component-emitter/1.3.0:
|
||||||
@@ -6377,7 +6381,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/component-inherit/0.0.3:
|
/component-inherit/0.0.3:
|
||||||
resolution: {integrity: sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=}
|
resolution: {integrity: sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/concat-map/0.0.1:
|
/concat-map/0.0.1:
|
||||||
@@ -8761,7 +8765,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/has-cors/1.1.0:
|
/has-cors/1.1.0:
|
||||||
resolution: {integrity: sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=}
|
resolution: {integrity: sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/has-flag/3.0.0:
|
/has-flag/3.0.0:
|
||||||
@@ -9060,7 +9064,7 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
/indexof/0.0.1:
|
/indexof/0.0.1:
|
||||||
resolution: {integrity: sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=}
|
resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/inflight/1.0.6:
|
/inflight/1.0.6:
|
||||||
@@ -12712,7 +12716,7 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/to-array/0.1.4:
|
/to-array/0.1.4:
|
||||||
resolution: {integrity: sha1-F+bBH3PdTz10zaek/zI46a2b+JA=}
|
resolution: {integrity: sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/to-fast-properties/2.0.0:
|
/to-fast-properties/2.0.0:
|
||||||
@@ -13742,29 +13746,10 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vite-plugin-pwa/0.13.1_3kw35epztoiwny7qtfesjexvtu:
|
/vite-plugin-pwa/0.13.1_vite@3.1.4:
|
||||||
resolution: {integrity: sha512-NR3dIa+o2hzlzo4lF4Gu0cYvoMjSw2DdRc6Epw1yjmCqWaGuN86WK9JqZie4arNlE1ZuWT3CLiMdiX5wcmmUmg==}
|
resolution: {integrity: sha512-NR3dIa+o2hzlzo4lF4Gu0cYvoMjSw2DdRc6Epw1yjmCqWaGuN86WK9JqZie4arNlE1ZuWT3CLiMdiX5wcmmUmg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^3.1.0
|
vite: ^3.1.0
|
||||||
workbox-window: ^6.5.4
|
|
||||||
dependencies:
|
|
||||||
debug: 4.3.4
|
|
||||||
fast-glob: 3.2.11
|
|
||||||
pretty-bytes: 6.0.0
|
|
||||||
rollup: 2.79.1
|
|
||||||
vite: 3.2.4
|
|
||||||
workbox-build: 6.5.4
|
|
||||||
workbox-window: 6.5.4
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@types/babel__core'
|
|
||||||
- supports-color
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/vite-plugin-pwa/0.13.1_bg4cnt4dy3xq3a47wkujd6ryzq:
|
|
||||||
resolution: {integrity: sha512-NR3dIa+o2hzlzo4lF4Gu0cYvoMjSw2DdRc6Epw1yjmCqWaGuN86WK9JqZie4arNlE1ZuWT3CLiMdiX5wcmmUmg==}
|
|
||||||
peerDependencies:
|
|
||||||
vite: ^3.1.0
|
|
||||||
workbox-window: ^6.5.4
|
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
fast-glob: 3.2.11
|
fast-glob: 3.2.11
|
||||||
@@ -13782,7 +13767,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-NR3dIa+o2hzlzo4lF4Gu0cYvoMjSw2DdRc6Epw1yjmCqWaGuN86WK9JqZie4arNlE1ZuWT3CLiMdiX5wcmmUmg==}
|
resolution: {integrity: sha512-NR3dIa+o2hzlzo4lF4Gu0cYvoMjSw2DdRc6Epw1yjmCqWaGuN86WK9JqZie4arNlE1ZuWT3CLiMdiX5wcmmUmg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^3.1.0
|
vite: ^3.1.0
|
||||||
workbox-window: ^6.5.4
|
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
fast-glob: 3.2.11
|
fast-glob: 3.2.11
|
||||||
@@ -13790,6 +13774,7 @@ packages:
|
|||||||
rollup: 2.79.1
|
rollup: 2.79.1
|
||||||
vite: 3.2.4_sass@1.53.0
|
vite: 3.2.4_sass@1.53.0
|
||||||
workbox-build: 6.5.4
|
workbox-build: 6.5.4
|
||||||
|
workbox-window: 6.5.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/babel__core'
|
- '@types/babel__core'
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -14826,7 +14811,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/yeast/0.1.2:
|
/yeast/0.1.2:
|
||||||
resolution: {integrity: sha1-AI4G2AlDIMNy28L47XagymyKxBk=}
|
resolution: {integrity: sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/yn/3.1.1:
|
/yn/3.1.1:
|
||||||
|
|||||||
Reference in New Issue
Block a user