Compare commits
4 Commits
feat/githu
...
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]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
X-Frame-Options = "DENY"
|
||||
X-Frame-Options = "SAMEORIGIN"
|
||||
X-XSS-Protection = "1; mode=block"
|
||||
|
||||
[[redirects]]
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"typecheck": "pnpm -r do-typecheck",
|
||||
"lintfix": "pnpm -r do-lintfix",
|
||||
"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": [
|
||||
"./packages/*"
|
||||
|
||||
@@ -29,10 +29,7 @@ import IconCheck from "~icons/lucide/check"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { authIdToken$ } from "~/helpers/fb/auth"
|
||||
|
||||
const userAuthToken = useReadonlyStream(authIdToken$, null)
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -53,8 +50,9 @@ const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
|
||||
// Copy user auth token to clipboard
|
||||
const copyUserAuthToken = () => {
|
||||
if (userAuthToken.value) {
|
||||
copyToClipboard(userAuthToken.value)
|
||||
const token = platform.auth.getDevOptsBackendIDToken()
|
||||
if (token) {
|
||||
copyToClipboard(token)
|
||||
copyIcon.value = IconCheck
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
} else {
|
||||
|
||||
@@ -219,7 +219,7 @@ import { showChat } from "@modules/crisp"
|
||||
import { useSetting } from "@composables/settings"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { invokeAction } from "@helpers/actions"
|
||||
@@ -236,7 +236,10 @@ const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
|
||||
|
||||
const navigatorShare = !!navigator.share
|
||||
|
||||
const currentUser = useReadonlyStream(currentUser$, null)
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
watch(
|
||||
() => ZEN_MODE.value,
|
||||
|
||||
@@ -171,11 +171,10 @@ import IconUploadCloud from "~icons/lucide/upload-cloud"
|
||||
import IconUserPlus from "~icons/lucide/user-plus"
|
||||
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
||||
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
||||
import { probableUser$ } from "@helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { invokeAction } from "@helpers/actions"
|
||||
import { platform } from "~/index"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -194,7 +193,10 @@ const mdAndLarger = breakpoints.greater("md")
|
||||
|
||||
const network = reactive(useNetwork())
|
||||
|
||||
const currentUser = useReadonlyStream(probableUser$, null)
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getProbableUserStream(),
|
||||
platform.auth.getProbableUser()
|
||||
)
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
|
||||
@@ -172,7 +172,7 @@ import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
|
||||
import { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
|
||||
import { StepReturnValue } from "~/helpers/import-export/steps"
|
||||
@@ -263,7 +263,10 @@ watch(inputChooseGistToImportFrom, (url) => {
|
||||
})
|
||||
|
||||
const myCollections = useReadonlyStream(restCollections$, [])
|
||||
const currentUser = useReadonlyStream(currentUser$, null)
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
const importerAction = async (stepResults: StepReturnValue[]) => {
|
||||
if (!importerModule.value) return
|
||||
|
||||
@@ -99,7 +99,7 @@ import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import { computed, ref } from "vue"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
@@ -120,7 +120,10 @@ const emit = defineEmits<{
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
const collections = useReadonlyStream(graphqlCollections$, [])
|
||||
const currentUser = useReadonlyStream(currentUser$, null)
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
|
||||
@@ -241,7 +241,7 @@ import {
|
||||
} from "~/helpers/backend/helpers"
|
||||
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
import { createCollectionGists } from "~/helpers/gist"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
|
||||
@@ -318,7 +318,10 @@ const confirmModalTitle = ref<string | null>(null)
|
||||
|
||||
const filterTexts = ref("")
|
||||
|
||||
const currentUser = useReadonlyStream(currentUser$, null)
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
const myCollections = useReadonlyStream(restCollections$, [], "deep")
|
||||
|
||||
// Export - Import refs
|
||||
@@ -1462,7 +1465,7 @@ const createCollectionGist = async () => {
|
||||
(result) => {
|
||||
toast.success(t("export.gist_created").toString())
|
||||
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 { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { onLoggedIn } from "@composables/auth"
|
||||
import { currentUserInfo$ } from "~/helpers/teams/BackendUserInfo"
|
||||
import { platform } from "~/platform"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
@@ -111,7 +111,10 @@ const emit = defineEmits<{
|
||||
(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 myTeams = useReadonlyStream(adapter.teamList$, null)
|
||||
@@ -138,7 +141,9 @@ watch(
|
||||
)
|
||||
|
||||
onLoggedIn(() => {
|
||||
adapter.initialize()
|
||||
try {
|
||||
adapter.initialize()
|
||||
} catch (e) {}
|
||||
})
|
||||
|
||||
const onTeamSelectIntersect = () => {
|
||||
|
||||
@@ -107,7 +107,7 @@ import IconDownload from "~icons/lucide/download"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import { computed, ref } from "vue"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
import axios from "axios"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
@@ -141,7 +141,10 @@ const t = useI18n()
|
||||
const loading = ref(false)
|
||||
|
||||
const myEnvironments = useReadonlyStream(environments$, [])
|
||||
const currentUser = useReadonlyStream(currentUser$, null)
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
@@ -187,7 +190,7 @@ const createEnvironmentGist = async () => {
|
||||
)
|
||||
|
||||
toast.success(t("export.gist_created").toString())
|
||||
window.open(res.html_url)
|
||||
window.open(res.data.html_url)
|
||||
} catch (e) {
|
||||
toast.error(t("error.something_went_wrong").toString())
|
||||
console.error(e)
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { isEqual } from "lodash-es"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
import { Team } from "~/helpers/backend/graphql"
|
||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
@@ -222,7 +222,10 @@ const globalEnvironment = computed(() => ({
|
||||
variables: globalEnv.value,
|
||||
}))
|
||||
|
||||
const currentUser = useReadonlyStream(currentUser$, null)
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
const updateSelectedTeam = (newSelectedTeam: SelectedTeam) => {
|
||||
environmentType.value.selectedTeam = newSelectedTeam
|
||||
|
||||
@@ -122,16 +122,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import {
|
||||
signInUserWithGoogle,
|
||||
signInUserWithGithub,
|
||||
signInUserWithMicrosoft,
|
||||
setProviderInfo,
|
||||
currentUser$,
|
||||
signInWithEmail,
|
||||
linkWithFBCredentialFromAuthError,
|
||||
getGithubCredentialFromResult,
|
||||
} from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
import IconGithub from "~icons/auth/github"
|
||||
import IconGoogle from "~icons/auth/google"
|
||||
import IconEmail from "~icons/auth/email"
|
||||
@@ -174,6 +165,8 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
|
||||
this.subscribeToStream(currentUser$, (user) => {
|
||||
if (user) this.hideModal()
|
||||
})
|
||||
@@ -186,8 +179,7 @@ export default defineComponent({
|
||||
this.signingInWithGoogle = true
|
||||
|
||||
try {
|
||||
await signInUserWithGoogle()
|
||||
this.showLoginSuccess()
|
||||
await platform.auth.signInUserWithGoogle()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
/*
|
||||
@@ -202,35 +194,32 @@ export default defineComponent({
|
||||
async signInWithGithub() {
|
||||
this.signingInWithGitHub = true
|
||||
|
||||
try {
|
||||
const result = await signInUserWithGithub()
|
||||
const credential = getGithubCredentialFromResult(result)!
|
||||
const token = credential.accessToken
|
||||
setProviderInfo(result.providerId!, token!)
|
||||
const result = await platform.auth.signInUserWithGithub()
|
||||
|
||||
this.showLoginSuccess()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
// 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()
|
||||
if (!result) {
|
||||
this.signingInWithGitHub = false
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
@@ -239,8 +228,8 @@ export default defineComponent({
|
||||
this.signingInWithMicrosoft = true
|
||||
|
||||
try {
|
||||
await signInUserWithMicrosoft()
|
||||
this.showLoginSuccess()
|
||||
await platform.auth.signInUserWithMicrosoft()
|
||||
// this.showLoginSuccess()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
/*
|
||||
@@ -259,11 +248,8 @@ export default defineComponent({
|
||||
async signInWithEmail() {
|
||||
this.signingInWithEmail = true
|
||||
|
||||
const actionCodeSettings = {
|
||||
url: `${import.meta.env.VITE_BASE_URL}/enter`,
|
||||
handleCodeInApp: true,
|
||||
}
|
||||
await signInWithEmail(this.form.email, actionCodeSettings)
|
||||
await platform.auth
|
||||
.signInWithEmail(this.form.email)
|
||||
.then(() => {
|
||||
this.mode = "email-sent"
|
||||
setLocalConfig("emailForSignIn", this.form.email)
|
||||
|
||||
@@ -22,7 +22,7 @@ import { ref } from "vue"
|
||||
import IconLogOut from "~icons/lucide/log-out"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { signOutUser } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
defineProps({
|
||||
outline: {
|
||||
@@ -47,7 +47,7 @@ const t = useI18n()
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await signOutUser()
|
||||
await platform.auth.signOutUser()
|
||||
toast.success(`${t("auth.logged_out")}`)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -82,7 +82,7 @@ import { ref, watchEffect, computed } from "vue"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
import { onAuthEvent, onLoggedIn } from "@composables/auth"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
@@ -102,7 +102,10 @@ usePageHead({
|
||||
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)
|
||||
watchEffect(() => (displayName.value = currentUser.value?.displayName))
|
||||
@@ -121,7 +124,9 @@ const loading = computed(
|
||||
)
|
||||
|
||||
onLoggedIn(() => {
|
||||
adapter.initialize()
|
||||
try {
|
||||
adapter.initialize()
|
||||
} catch (e) {}
|
||||
})
|
||||
|
||||
onAuthEvent((ev) => {
|
||||
|
||||
@@ -109,7 +109,7 @@ import { useI18n } from "~/composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { GetMyTeamsDocument, GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { deleteUser } from "~/helpers/backend/mutations/Profile"
|
||||
import { signOutUser } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -162,7 +162,7 @@ const deleteUserAccount = async () => {
|
||||
deletingUser.value = false
|
||||
showDeleteAccountModal.value = false
|
||||
toast.success(t("settings.account_deleted"))
|
||||
signOutUser()
|
||||
platform.auth.signOutUser()
|
||||
router.push(`/`)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -108,7 +108,9 @@ const loading = computed(
|
||||
)
|
||||
|
||||
onLoggedIn(() => {
|
||||
adapter.initialize()
|
||||
try {
|
||||
adapter.initialize()
|
||||
} catch (e) {}
|
||||
})
|
||||
|
||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import {
|
||||
currentUser$,
|
||||
HoppUser,
|
||||
AuthEvent,
|
||||
authEvents$,
|
||||
authIdToken$,
|
||||
} from "@helpers/fb/auth"
|
||||
import {
|
||||
map,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
Subscription,
|
||||
combineLatestWith,
|
||||
} from "rxjs"
|
||||
import { onBeforeUnmount, onMounted } from "vue"
|
||||
import { platform } from "~/platform"
|
||||
import { AuthEvent, HoppUser } from "~/platform/auth"
|
||||
import { Subscription } from "rxjs"
|
||||
import { onBeforeUnmount, onMounted, watch, WatchStopHandle } from "vue"
|
||||
import { useReadonlyStream } from "./stream"
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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(() => {
|
||||
sub = currentUser$
|
||||
.pipe(
|
||||
// We don't consider the state as logged in unless we also have an id token
|
||||
combineLatestWith(authIdToken$),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
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!)
|
||||
})
|
||||
if (currentUser.value) exec(currentUser.value)
|
||||
|
||||
watchStop = watch(currentUser, (newVal, prev) => {
|
||||
if (prev === null && newVal !== null) {
|
||||
exec(newVal)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
sub?.unsubscribe()
|
||||
watchStop?.()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,6 +46,8 @@ export function onLoggedIn(exec: (user: HoppUser) => void) {
|
||||
* @param func A function which accepts an event
|
||||
*/
|
||||
export function onAuthEvent(func: (ev: AuthEvent) => void) {
|
||||
const authEvents$ = platform.auth.getAuthEventsStream()
|
||||
|
||||
let sub: Subscription | null = null
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CombinedError,
|
||||
Operation,
|
||||
OperationResult,
|
||||
Client,
|
||||
} from "@urql/core"
|
||||
import { authExchange } from "@urql/exchange-auth"
|
||||
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 { subscribe, pipe as wonkaPipe } from "wonka"
|
||||
import { filter, map, Subject } from "rxjs"
|
||||
import {
|
||||
authIdToken$,
|
||||
getAuthIDToken,
|
||||
probableUser$,
|
||||
waitProbableLoginToConfirm,
|
||||
} from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
// TODO: Implement caching
|
||||
|
||||
@@ -57,11 +53,7 @@ export const gqlClientError$ = new Subject<GQLClientErrorEvent>()
|
||||
const createSubscriptionClient = () => {
|
||||
return new SubscriptionClient(BACKEND_WS_URL, {
|
||||
reconnect: true,
|
||||
connectionParams: () => {
|
||||
return {
|
||||
authorization: `Bearer ${authIdToken$.value}`,
|
||||
}
|
||||
},
|
||||
connectionParams: () => platform.auth.getBackendHeaders(),
|
||||
connectionCallback(error) {
|
||||
if (error?.length > 0) {
|
||||
gqlClientError$.next({
|
||||
@@ -79,7 +71,7 @@ const createHoppClient = () => {
|
||||
dedupExchange,
|
||||
authExchange({
|
||||
addAuthToOperation({ authState, operation }) {
|
||||
if (!authState || !authState.authToken) {
|
||||
if (!authState) {
|
||||
return operation
|
||||
}
|
||||
|
||||
@@ -88,28 +80,29 @@ const createHoppClient = () => {
|
||||
? operation.context.fetchOptions()
|
||||
: operation.context.fetchOptions || {}
|
||||
|
||||
const authHeaders = platform.auth.getBackendHeaders()
|
||||
|
||||
return makeOperation(operation.kind, operation, {
|
||||
...operation.context,
|
||||
fetchOptions: {
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
...fetchOptions.headers,
|
||||
Authorization: `Bearer ${authState.authToken}`,
|
||||
...authHeaders,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
willAuthError({ authState }) {
|
||||
return !authState || !authState.authToken
|
||||
willAuthError() {
|
||||
return platform.auth.willBackendHaveAuthError()
|
||||
},
|
||||
getAuth: async () => {
|
||||
if (!probableUser$.value) return { authToken: null }
|
||||
const probableUser = platform.auth.getProbableUser()
|
||||
|
||||
await waitProbableLoginToConfirm()
|
||||
if (probableUser !== null)
|
||||
await platform.auth.waitProbableLoginToConfirm()
|
||||
|
||||
return {
|
||||
authToken: getAuthIDToken(),
|
||||
}
|
||||
return {}
|
||||
},
|
||||
}),
|
||||
fetchExchange,
|
||||
@@ -137,31 +130,40 @@ const createHoppClient = () => {
|
||||
return createClient({
|
||||
url: BACKEND_GQL_URL,
|
||||
exchanges,
|
||||
...(platform.auth.getGQLClientOptions
|
||||
? platform.auth.getGQLClientOptions()
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
|
||||
let subscriptionClient: SubscriptionClient | null
|
||||
export const client = ref(createHoppClient())
|
||||
|
||||
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 const client = ref<Client>()
|
||||
|
||||
export function initBackendGQLClient() {
|
||||
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> = {
|
||||
query: TypedDocumentNode<T, V>
|
||||
@@ -185,7 +187,7 @@ export const runGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
|
||||
args: RunQueryOptions<DocType, DocVarType>
|
||||
): Promise<E.Either<GQLError<DocErrorType>, DocType>> => {
|
||||
const request = createRequest<DocType, DocVarType>(args.query, args.variables)
|
||||
const source = client.value.executeQuery(request, {
|
||||
const source = client.value!.executeQuery(request, {
|
||||
requestPolicy: "network-only",
|
||||
})
|
||||
|
||||
@@ -250,7 +252,7 @@ export const runGQLSubscription = <
|
||||
) => {
|
||||
const result$ = new Subject<E.Either<GQLError<DocErrorType>, DocType>>()
|
||||
|
||||
const source = client.value.executeSubscription(
|
||||
const source = client.value!.executeSubscription(
|
||||
createRequest(args.query, args.variables)
|
||||
)
|
||||
|
||||
@@ -342,8 +344,8 @@ export const runMutation = <
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
client.value
|
||||
.mutation(mutation, variables, {
|
||||
client
|
||||
.value!.mutation(mutation, variables, {
|
||||
requestPolicy: "cache-and-network",
|
||||
...additionalConfig,
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
setUserId,
|
||||
setUserProperties,
|
||||
} from "firebase/analytics"
|
||||
import { authEvents$ } from "./auth"
|
||||
import { platform } from "~/platform"
|
||||
import {
|
||||
HoppAccentColor,
|
||||
HoppBgColor,
|
||||
@@ -42,13 +42,15 @@ export function initAnalytics() {
|
||||
}
|
||||
|
||||
function initLoginListeners() {
|
||||
const authEvents$ = platform.auth.getAuthEventsStream()
|
||||
|
||||
authEvents$.subscribe((ev) => {
|
||||
if (ev.event === "login") {
|
||||
if (settingsStore.value.TELEMETRY_ENABLED && analytics) {
|
||||
setUserId(analytics, ev.user.uid)
|
||||
|
||||
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") {
|
||||
|
||||
@@ -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,
|
||||
translateToNewGQLCollection,
|
||||
} from "@hoppscotch/data"
|
||||
import { currentUser$ } from "./auth"
|
||||
import { platform } from "~/platform"
|
||||
import {
|
||||
restCollections$,
|
||||
graphqlCollections$,
|
||||
@@ -44,20 +44,22 @@ export async function writeCollections(
|
||||
collection: any[],
|
||||
flag: CollectionFlags
|
||||
) {
|
||||
if (currentUser$.value === null)
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser === null)
|
||||
throw new Error("User not logged in to write collections")
|
||||
|
||||
const cl = {
|
||||
updatedOn: new Date(),
|
||||
author: currentUser$.value.uid,
|
||||
author_name: currentUser$.value.displayName,
|
||||
author_image: currentUser$.value.photoURL,
|
||||
author: currentUser.uid,
|
||||
author_name: currentUser.displayName,
|
||||
author_image: currentUser.photoURL,
|
||||
collection,
|
||||
}
|
||||
|
||||
try {
|
||||
await setDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, flag, "sync"),
|
||||
doc(getFirestore(), "users", currentUser.uid, flag, "sync"),
|
||||
cl
|
||||
)
|
||||
} catch (e) {
|
||||
@@ -67,10 +69,13 @@ export async function writeCollections(
|
||||
}
|
||||
|
||||
export function initCollections() {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
const restCollSub = restCollections$.subscribe((collections) => {
|
||||
if (
|
||||
loadedRESTCollections &&
|
||||
currentUser$.value &&
|
||||
currentUser &&
|
||||
settingsStore.value.syncCollections
|
||||
) {
|
||||
writeCollections(collections, "collections")
|
||||
@@ -80,7 +85,7 @@ export function initCollections() {
|
||||
const gqlCollSub = graphqlCollections$.subscribe((collections) => {
|
||||
if (
|
||||
loadedGraphqlCollections &&
|
||||
currentUser$.value &&
|
||||
currentUser &&
|
||||
settingsStore.value.syncCollections
|
||||
) {
|
||||
writeCollections(collections, "collectionsGraphql")
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
onSnapshot,
|
||||
setDoc,
|
||||
} from "firebase/firestore"
|
||||
import { currentUser$ } from "./auth"
|
||||
import { platform } from "~/platform"
|
||||
import {
|
||||
environments$,
|
||||
globalEnv$,
|
||||
@@ -32,26 +32,22 @@ let loadedEnvironments = false
|
||||
let loadedGlobals = true
|
||||
|
||||
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")
|
||||
|
||||
const ev = {
|
||||
updatedOn: new Date(),
|
||||
author: currentUser$.value.uid,
|
||||
author_name: currentUser$.value.displayName,
|
||||
author_image: currentUser$.value.photoURL,
|
||||
author: currentUser.uid,
|
||||
author_name: currentUser.displayName,
|
||||
author_image: currentUser.photoURL,
|
||||
environment,
|
||||
}
|
||||
|
||||
try {
|
||||
await setDoc(
|
||||
doc(
|
||||
getFirestore(),
|
||||
"users",
|
||||
currentUser$.value.uid,
|
||||
"environments",
|
||||
"sync"
|
||||
),
|
||||
doc(getFirestore(), "users", currentUser.uid, "environments", "sync"),
|
||||
ev
|
||||
)
|
||||
} catch (e) {
|
||||
@@ -61,20 +57,22 @@ async function writeEnvironments(environment: Environment[]) {
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
const ev = {
|
||||
updatedOn: new Date(),
|
||||
author: currentUser$.value.uid,
|
||||
author_name: currentUser$.value.displayName,
|
||||
author_image: currentUser$.value.photoURL,
|
||||
author: currentUser.uid,
|
||||
author_name: currentUser.displayName,
|
||||
author_image: currentUser.photoURL,
|
||||
variables,
|
||||
}
|
||||
|
||||
try {
|
||||
await setDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, "globalEnv", "sync"),
|
||||
doc(getFirestore(), "users", currentUser.uid, "globalEnv", "sync"),
|
||||
ev
|
||||
)
|
||||
} catch (e) {
|
||||
@@ -84,9 +82,12 @@ async function writeGlobalEnvironment(variables: Environment["variables"]) {
|
||||
}
|
||||
|
||||
export function initEnvironments() {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
const envListenSub = environments$.subscribe((envs) => {
|
||||
if (
|
||||
currentUser$.value &&
|
||||
currentUser &&
|
||||
settingsStore.value.syncEnvironments &&
|
||||
loadedEnvironments
|
||||
) {
|
||||
@@ -95,11 +96,7 @@ export function initEnvironments() {
|
||||
})
|
||||
|
||||
const globalListenSub = globalEnv$.subscribe((vars) => {
|
||||
if (
|
||||
currentUser$.value &&
|
||||
settingsStore.value.syncEnvironments &&
|
||||
loadedGlobals
|
||||
) {
|
||||
if (currentUser && settingsStore.value.syncEnvironments && loadedGlobals) {
|
||||
writeGlobalEnvironment(vars)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
updateDoc,
|
||||
} from "firebase/firestore"
|
||||
import { FormDataKeyValue } from "@hoppscotch/data"
|
||||
import { currentUser$ } from "./auth"
|
||||
import { platform } from "~/platform"
|
||||
import { getSettingSubject, settingsStore } from "~/newstore/settings"
|
||||
import {
|
||||
GQLHistoryEntry,
|
||||
@@ -76,7 +76,9 @@ async function writeHistory(
|
||||
? purgeFormDataFromRequest(entry as RESTHistoryEntry)
|
||||
: entry
|
||||
|
||||
if (currentUser$.value == null)
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser === null)
|
||||
throw new Error("User not logged in to sync history")
|
||||
|
||||
const hs = {
|
||||
@@ -85,10 +87,7 @@ async function writeHistory(
|
||||
}
|
||||
|
||||
try {
|
||||
await addDoc(
|
||||
collection(getFirestore(), "users", currentUser$.value.uid, col),
|
||||
hs
|
||||
)
|
||||
await addDoc(collection(getFirestore(), "users", currentUser.uid, col), hs)
|
||||
} catch (e) {
|
||||
console.error("error writing to history", hs, e)
|
||||
throw e
|
||||
@@ -99,12 +98,14 @@ async function deleteHistory(
|
||||
entry: (RESTHistoryEntry | GQLHistoryEntry) & { id: string },
|
||||
col: HistoryFBCollections
|
||||
) {
|
||||
if (currentUser$.value == null)
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser === null)
|
||||
throw new Error("User not logged in to delete history")
|
||||
|
||||
try {
|
||||
await deleteDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, col, entry.id)
|
||||
doc(getFirestore(), "users", currentUser.uid, col, entry.id)
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("error deleting history", entry, e)
|
||||
@@ -113,11 +114,13 @@ async function deleteHistory(
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
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)))
|
||||
@@ -127,12 +130,13 @@ async function toggleStar(
|
||||
entry: (RESTHistoryEntry | GQLHistoryEntry) & { id: string },
|
||||
col: HistoryFBCollections
|
||||
) {
|
||||
if (currentUser$.value == null)
|
||||
throw new Error("User not logged in to toggle star")
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser === null) throw new Error("User not logged in to toggle star")
|
||||
|
||||
try {
|
||||
await updateDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, col, entry.id),
|
||||
doc(getFirestore(), "users", currentUser.uid, col, entry.id),
|
||||
{ star: !entry.star }
|
||||
)
|
||||
} catch (e) {
|
||||
@@ -142,12 +146,11 @@ async function toggleStar(
|
||||
}
|
||||
|
||||
export function initHistory() {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
const restHistorySub = restHistoryStore.dispatches$.subscribe((dispatch) => {
|
||||
if (
|
||||
loadedRESTHistory &&
|
||||
currentUser$.value &&
|
||||
settingsStore.value.syncHistory
|
||||
) {
|
||||
if (loadedRESTHistory && currentUser && settingsStore.value.syncHistory) {
|
||||
if (dispatch.dispatcher === "addEntry") {
|
||||
writeHistory(dispatch.payload.entry, "history")
|
||||
} else if (dispatch.dispatcher === "deleteEntry") {
|
||||
@@ -164,7 +167,7 @@ export function initHistory() {
|
||||
(dispatch) => {
|
||||
if (
|
||||
loadedGraphqlHistory &&
|
||||
currentUser$.value &&
|
||||
currentUser &&
|
||||
settingsStore.value.syncHistory
|
||||
) {
|
||||
if (dispatch.dispatcher === "addEntry") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { initializeApp } from "firebase/app"
|
||||
import { platform } from "~/platform"
|
||||
import { initAnalytics } from "./analytics"
|
||||
import { initAuth } from "./auth"
|
||||
import { initCollections } from "./collections"
|
||||
import { initEnvironments } from "./environments"
|
||||
import { initHistory } from "./history"
|
||||
@@ -24,7 +24,7 @@ export function initializeFirebase() {
|
||||
try {
|
||||
initializeApp(firebaseConfig)
|
||||
|
||||
initAuth()
|
||||
platform.auth.performAuthInit()
|
||||
initSettings()
|
||||
initCollections()
|
||||
initHistory()
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
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"
|
||||
|
||||
/**
|
||||
@@ -44,7 +45,7 @@ function writeCurrentRequest(user: HoppUser, request: HoppRESTRequest) {
|
||||
* @returns Fetched request object if exists else null
|
||||
*/
|
||||
export async function loadRequestFromSync(): Promise<HoppRESTRequest | null> {
|
||||
const currentUser = currentUser$.value
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (!currentUser)
|
||||
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.
|
||||
*/
|
||||
export function startRequestSync(): Subscription {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
|
||||
const sub = combineLatest([
|
||||
currentUser$,
|
||||
restRequest$.pipe(distinctUntilChanged()),
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
onSnapshot,
|
||||
setDoc,
|
||||
} from "firebase/firestore"
|
||||
import { currentUser$ } from "./auth"
|
||||
import { platform } from "~/platform"
|
||||
import { applySetting, settingsStore, SettingsType } from "~/newstore/settings"
|
||||
|
||||
/**
|
||||
@@ -20,21 +20,23 @@ let loadedSettings = false
|
||||
* Write Transform
|
||||
*/
|
||||
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")
|
||||
|
||||
const st = {
|
||||
updatedOn: new Date(),
|
||||
author: currentUser$.value.uid,
|
||||
author_name: currentUser$.value.displayName,
|
||||
author_image: currentUser$.value.photoURL,
|
||||
author: currentUser.uid,
|
||||
author_name: currentUser.displayName,
|
||||
author_image: currentUser.photoURL,
|
||||
name: setting,
|
||||
value,
|
||||
}
|
||||
|
||||
try {
|
||||
await setDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, "settings", setting),
|
||||
doc(getFirestore(), "users", currentUser.uid, "settings", setting),
|
||||
st
|
||||
)
|
||||
} catch (e) {
|
||||
@@ -44,8 +46,11 @@ async function writeSettings(setting: string, value: any) {
|
||||
}
|
||||
|
||||
export function initSettings() {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
settingsStore.dispatches$.subscribe((dispatch) => {
|
||||
if (currentUser$.value && loadedSettings) {
|
||||
if (currentUser && loadedSettings) {
|
||||
if (dispatch.dispatcher === "bulkApplySettings") {
|
||||
Object.keys(dispatch.payload).forEach((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 { GQLError, runGQLQuery } from "../backend/GQLClient"
|
||||
import { GetMyTeamsDocument, GetMyTeamsQuery } from "../backend/graphql"
|
||||
import { authIdToken$ } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const BACKEND_PAGE_SIZE = 10
|
||||
const POLL_DURATION = 10000
|
||||
@@ -47,8 +47,10 @@ export default class TeamListAdapter {
|
||||
}
|
||||
|
||||
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 (!authIdToken$.value) return
|
||||
if (!currentUser) return
|
||||
|
||||
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 { performMigrations } from "./helpers/migrations"
|
||||
import { initializeFirebase } from "./helpers/fb"
|
||||
import { initUserInfo } from "./helpers/teams/BackendUserInfo"
|
||||
import { initBackendGQLClient } from "./helpers/backend/GQLClient"
|
||||
import { HOPP_MODULES } from "@modules/."
|
||||
|
||||
import "virtual:windi.css"
|
||||
@@ -12,33 +13,16 @@ import "nprogress/nprogress.css"
|
||||
|
||||
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) {
|
||||
platform = platformDef
|
||||
setPlatformDef(platformDef)
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// Some basic work that needs to be done before module inits even
|
||||
initializeFirebase()
|
||||
initBackendGQLClient()
|
||||
setupLocalPersistence()
|
||||
performMigrations()
|
||||
initUserInfo()
|
||||
|
||||
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app))
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { settingsStore } from "~/newstore/settings"
|
||||
import { App } from "vue"
|
||||
import { APP_IS_IN_DEV_MODE } from "~/helpers/dev"
|
||||
import { gqlClientError$ } from "~/helpers/backend/GQLClient"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
/**
|
||||
* The tag names we allow giving to Sentry
|
||||
@@ -164,6 +164,8 @@ function subscribeToAppEventsForReporting() {
|
||||
* additional data tags for the error reporting
|
||||
*/
|
||||
function subscribeForAppDataTags() {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
|
||||
currentUser$.subscribe((user) => {
|
||||
if (sentryActive) {
|
||||
Sentry.setTag("user_logged_in", !!user)
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
import { defineComponent } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { initializeFirebase } from "~/helpers/fb"
|
||||
import { isSignInWithEmailLink, signInWithEmailLink } from "~/helpers/fb/auth"
|
||||
import { getLocalConfig, removeLocalConfig } from "~/newstore/localpersistence"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
@@ -29,29 +28,14 @@ export default defineComponent({
|
||||
initializeFirebase()
|
||||
},
|
||||
async mounted() {
|
||||
if (isSignInWithEmailLink(window.location.href)) {
|
||||
this.signingInWithEmail = true
|
||||
this.signingInWithEmail = true
|
||||
|
||||
let email = getLocalConfig("emailForSignIn")
|
||||
|
||||
if (!email) {
|
||||
email = window.prompt(
|
||||
"Please provide your email for confirmation"
|
||||
) as string
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
try {
|
||||
await platform.auth.processMagicLink()
|
||||
} catch (e) {
|
||||
this.error = e.message
|
||||
} finally {
|
||||
this.signingInWithEmail = false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -156,7 +156,7 @@ import {
|
||||
} from "~/helpers/backend/graphql"
|
||||
import { acceptTeamInvitation } from "~/helpers/backend/mutations/TeamInvitation"
|
||||
import { initializeFirebase } from "~/helpers/fb"
|
||||
import { currentUser$, probableUser$ } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
import { onLoggedIn } from "@composables/auth"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
@@ -197,8 +197,15 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
const probableUser = useReadonlyStream(probableUser$, null)
|
||||
const currentUser = useReadonlyStream(currentUser$, null)
|
||||
const probableUser = useReadonlyStream(
|
||||
platform.auth.getProbableUserStream(),
|
||||
platform.auth.getProbableUser()
|
||||
)
|
||||
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
const loadingCurrentUser = computed(() => {
|
||||
if (!probableUser.value) return false
|
||||
|
||||
@@ -211,13 +211,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect, computed } from "vue"
|
||||
import {
|
||||
currentUser$,
|
||||
probableUser$,
|
||||
setDisplayName,
|
||||
setEmailAddress,
|
||||
verifyEmailAddress,
|
||||
} from "~/helpers/fb/auth"
|
||||
|
||||
import { platform } from "~/platform"
|
||||
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
@@ -247,8 +243,14 @@ usePageHead({
|
||||
const SYNC_COLLECTIONS = useSetting("syncCollections")
|
||||
const SYNC_ENVIRONMENTS = useSetting("syncEnvironments")
|
||||
const SYNC_HISTORY = useSetting("syncHistory")
|
||||
const currentUser = useReadonlyStream(currentUser$, null)
|
||||
const probableUser = useReadonlyStream(probableUser$, null)
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
const probableUser = useReadonlyStream(
|
||||
platform.auth.getProbableUserStream(),
|
||||
platform.auth.getProbableUser()
|
||||
)
|
||||
|
||||
const loadingCurrentUser = computed(() => {
|
||||
if (!probableUser.value) return false
|
||||
@@ -262,7 +264,8 @@ watchEffect(() => (displayName.value = currentUser.value?.displayName))
|
||||
|
||||
const updateDisplayName = () => {
|
||||
updatingDisplayName.value = true
|
||||
setDisplayName(displayName.value as string)
|
||||
platform.auth
|
||||
.setDisplayName(displayName.value as string)
|
||||
.then(() => {
|
||||
toast.success(`${t("profile.updated")}`)
|
||||
})
|
||||
@@ -280,7 +283,8 @@ watchEffect(() => (emailAddress.value = currentUser.value?.email))
|
||||
|
||||
const updateEmailAddress = () => {
|
||||
updatingEmailAddress.value = true
|
||||
setEmailAddress(emailAddress.value as string)
|
||||
platform.auth
|
||||
.setEmailAddress(emailAddress.value as string)
|
||||
.then(() => {
|
||||
toast.success(`${t("profile.updated")}`)
|
||||
})
|
||||
@@ -296,7 +300,8 @@ const verifyingEmailAddress = ref(false)
|
||||
|
||||
const sendEmailVerification = () => {
|
||||
verifyingEmailAddress.value = true
|
||||
verifyEmailAddress()
|
||||
platform.auth
|
||||
.verifyEmailAddress()
|
||||
.then(() => {
|
||||
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"
|
||||
|
||||
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",
|
||||
plugins: [HstVue()],
|
||||
})
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"build": "vite build",
|
||||
"story:dev": "histoire dev",
|
||||
"story:build": "histoire build",
|
||||
"story:preview": "histoire preview"
|
||||
"story:preview": "histoire preview",
|
||||
"do-build-ui": "pnpm run story:build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hoppscotch - Open source API development ecosystem</title>
|
||||
<title>Hoppscotch • Open source API development ecosystem</title>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"dependencies": {
|
||||
"@hoppscotch/common": "workspace:^",
|
||||
"buffer": "^6.0.3",
|
||||
"firebase": "^9.8.4",
|
||||
"process": "^0.11.10",
|
||||
"rxjs": "^7.5.5",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"util": "^0.12.4",
|
||||
"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 { 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-pages: 0.26.0_vnheu5mvzzbfbuhqo4shkhdhei
|
||||
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-windicss: 1.8.8_vite@3.1.4
|
||||
vue-tsc: 0.38.2_typescript@4.7.4
|
||||
@@ -561,7 +561,9 @@ importers:
|
||||
eslint: ^8.28.0
|
||||
eslint-plugin-prettier: ^4.2.1
|
||||
eslint-plugin-vue: ^9.5.1
|
||||
firebase: ^9.8.4
|
||||
process: ^0.11.10
|
||||
rxjs: ^7.5.5
|
||||
stream-browserify: ^3.0.0
|
||||
typescript: ^4.6.4
|
||||
unplugin-icons: ^0.14.9
|
||||
@@ -584,7 +586,9 @@ importers:
|
||||
dependencies:
|
||||
'@hoppscotch/common': link:../hoppscotch-common
|
||||
buffer: 6.0.3
|
||||
firebase: 9.8.4
|
||||
process: 0.11.10
|
||||
rxjs: 7.5.5
|
||||
stream-browserify: 3.0.0
|
||||
util: 0.12.4
|
||||
vue: 3.2.45
|
||||
@@ -610,7 +614,7 @@ importers:
|
||||
vite-plugin-inspect: 0.7.4_vite@3.2.4
|
||||
vite-plugin-pages: 0.26.0_vite@3.2.4
|
||||
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-vue-layouts: 0.7.0_vite@3.2.4+vue@3.2.45
|
||||
vite-plugin-windicss: 1.8.8_vite@3.2.4
|
||||
@@ -5566,7 +5570,7 @@ packages:
|
||||
dev: true
|
||||
|
||||
/after/0.8.2:
|
||||
resolution: {integrity: sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=}
|
||||
resolution: {integrity: sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==}
|
||||
dev: false
|
||||
|
||||
/agent-base/6.0.2:
|
||||
@@ -5923,7 +5927,7 @@ packages:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
/base64-arraybuffer/0.1.4:
|
||||
resolution: {integrity: sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=}
|
||||
resolution: {integrity: sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
dev: false
|
||||
|
||||
@@ -6369,7 +6373,7 @@ packages:
|
||||
dev: true
|
||||
|
||||
/component-bind/1.0.0:
|
||||
resolution: {integrity: sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=}
|
||||
resolution: {integrity: sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==}
|
||||
dev: false
|
||||
|
||||
/component-emitter/1.3.0:
|
||||
@@ -6377,7 +6381,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
/component-inherit/0.0.3:
|
||||
resolution: {integrity: sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=}
|
||||
resolution: {integrity: sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==}
|
||||
dev: false
|
||||
|
||||
/concat-map/0.0.1:
|
||||
@@ -8761,7 +8765,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
/has-cors/1.1.0:
|
||||
resolution: {integrity: sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=}
|
||||
resolution: {integrity: sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==}
|
||||
dev: false
|
||||
|
||||
/has-flag/3.0.0:
|
||||
@@ -9060,7 +9064,7 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/indexof/0.0.1:
|
||||
resolution: {integrity: sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=}
|
||||
resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==}
|
||||
dev: false
|
||||
|
||||
/inflight/1.0.6:
|
||||
@@ -12712,7 +12716,7 @@ packages:
|
||||
dev: true
|
||||
|
||||
/to-array/0.1.4:
|
||||
resolution: {integrity: sha1-F+bBH3PdTz10zaek/zI46a2b+JA=}
|
||||
resolution: {integrity: sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==}
|
||||
dev: false
|
||||
|
||||
/to-fast-properties/2.0.0:
|
||||
@@ -13742,29 +13746,10 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/vite-plugin-pwa/0.13.1_3kw35epztoiwny7qtfesjexvtu:
|
||||
/vite-plugin-pwa/0.13.1_vite@3.1.4:
|
||||
resolution: {integrity: sha512-NR3dIa+o2hzlzo4lF4Gu0cYvoMjSw2DdRc6Epw1yjmCqWaGuN86WK9JqZie4arNlE1ZuWT3CLiMdiX5wcmmUmg==}
|
||||
peerDependencies:
|
||||
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:
|
||||
debug: 4.3.4
|
||||
fast-glob: 3.2.11
|
||||
@@ -13782,7 +13767,6 @@ packages:
|
||||
resolution: {integrity: sha512-NR3dIa+o2hzlzo4lF4Gu0cYvoMjSw2DdRc6Epw1yjmCqWaGuN86WK9JqZie4arNlE1ZuWT3CLiMdiX5wcmmUmg==}
|
||||
peerDependencies:
|
||||
vite: ^3.1.0
|
||||
workbox-window: ^6.5.4
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
fast-glob: 3.2.11
|
||||
@@ -13790,6 +13774,7 @@ packages:
|
||||
rollup: 2.79.1
|
||||
vite: 3.2.4_sass@1.53.0
|
||||
workbox-build: 6.5.4
|
||||
workbox-window: 6.5.4
|
||||
transitivePeerDependencies:
|
||||
- '@types/babel__core'
|
||||
- supports-color
|
||||
@@ -14826,7 +14811,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
/yeast/0.1.2:
|
||||
resolution: {integrity: sha1-AI4G2AlDIMNy28L47XagymyKxBk=}
|
||||
resolution: {integrity: sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==}
|
||||
dev: false
|
||||
|
||||
/yn/3.1.1:
|
||||
|
||||
Reference in New Issue
Block a user