feat: dynamically load enabled auth providers (#3646)
This commit is contained in:
@@ -308,7 +308,8 @@
|
||||
"proxy_error": "Proxy error",
|
||||
"script_fail": "Could not execute pre-request script",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"test_script_fail": "Could not execute post-request script"
|
||||
"test_script_fail": "Could not execute post-request script",
|
||||
"authproviders_load_error": "Unable to load auth providers"
|
||||
},
|
||||
"export": {
|
||||
"as_json": "Export as JSON",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||
<FirebaseLogin v-if="showLogin" @hide-modal="showLogin = false" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,64 +1,71 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('auth.login_to_hoppscotch')}`"
|
||||
styles="sm:max-w-md"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
|
||||
<HoppSmartItem
|
||||
v-for="provider in allowedAuthProviders"
|
||||
:key="provider.id"
|
||||
:loading="provider.isLoading.value"
|
||||
:icon="provider.icon"
|
||||
:label="provider.label"
|
||||
@click="provider.action"
|
||||
/>
|
||||
|
||||
<hr v-if="additonalLoginItems.length > 0" />
|
||||
|
||||
<HoppSmartItem
|
||||
v-for="loginItem in additonalLoginItems"
|
||||
:key="loginItem.id"
|
||||
:icon="loginItem.icon"
|
||||
:label="loginItem.text(t)"
|
||||
@click="doAdditionalLoginItemClickAction(loginItem)"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
v-if="mode === 'email'"
|
||||
class="flex flex-col space-y-2"
|
||||
@submit.prevent="signInWithEmail"
|
||||
>
|
||||
<HoppSmartInput
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
placeholder=" "
|
||||
:label="t('auth.email')"
|
||||
input-styles="floating-input"
|
||||
/>
|
||||
|
||||
<HoppButtonPrimary
|
||||
:loading="signingInWithEmail"
|
||||
type="submit"
|
||||
:label="`${t('auth.send_magic_link')}`"
|
||||
/>
|
||||
</form>
|
||||
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
|
||||
<div class="flex max-w-md flex-col items-center justify-center">
|
||||
<icon-lucide-inbox class="h-6 w-6 text-accent" />
|
||||
<h3 class="my-2 text-center text-lg">
|
||||
{{ t("auth.we_sent_magic_link") }}
|
||||
</h3>
|
||||
<p class="text-center">
|
||||
{{
|
||||
t("auth.we_sent_magic_link_description", { email: form.email })
|
||||
}}
|
||||
</p>
|
||||
<template v-if="isLoadingAllowedAuthProviders">
|
||||
<div class="flex justify-center">
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
|
||||
<HoppSmartItem
|
||||
v-for="provider in allowedAuthProviders"
|
||||
:key="provider.id"
|
||||
:loading="provider.isLoading.value"
|
||||
:icon="provider.icon"
|
||||
:label="provider.label"
|
||||
@click="provider.action"
|
||||
/>
|
||||
|
||||
<hr v-if="additonalLoginItems.length > 0" />
|
||||
|
||||
<HoppSmartItem
|
||||
v-for="loginItem in additonalLoginItems"
|
||||
:key="loginItem.id"
|
||||
:icon="loginItem.icon"
|
||||
:label="loginItem.text(t)"
|
||||
@click="doAdditionalLoginItemClickAction(loginItem)"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
v-if="mode === 'email'"
|
||||
class="flex flex-col space-y-2"
|
||||
@submit.prevent="signInWithEmail"
|
||||
>
|
||||
<HoppSmartInput
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
placeholder=" "
|
||||
:label="t('auth.email')"
|
||||
input-styles="floating-input"
|
||||
/>
|
||||
|
||||
<HoppButtonPrimary
|
||||
:loading="signingInWithEmail"
|
||||
type="submit"
|
||||
:label="`${t('auth.send_magic_link')}`"
|
||||
/>
|
||||
</form>
|
||||
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
|
||||
<div class="flex max-w-md flex-col items-center justify-center">
|
||||
<icon-lucide-inbox class="h-6 w-6 text-accent" />
|
||||
<h3 class="my-2 text-center text-lg">
|
||||
{{ t("auth.we_sent_magic_link") }}
|
||||
</h3>
|
||||
<p class="text-center">
|
||||
{{
|
||||
t("auth.we_sent_magic_link_description", { email: form.email })
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div
|
||||
@@ -109,7 +116,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, computed, onMounted, ref } from "vue"
|
||||
import { Ref, onMounted, ref } from "vue"
|
||||
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useStreamSubscriber } from "@composables/stream"
|
||||
@@ -127,9 +134,7 @@ import { useService } from "dioc/vue"
|
||||
import { LoginItemDef } from "~/platform/auth"
|
||||
import { PersistenceService } from "~/services/persistence"
|
||||
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
@@ -145,6 +150,8 @@ const form = {
|
||||
email: "",
|
||||
}
|
||||
|
||||
const isLoadingAllowedAuthProviders = ref(true)
|
||||
|
||||
const signingInWithGoogle = ref(false)
|
||||
const signingInWithGitHub = ref(false)
|
||||
const signingInWithMicrosoft = ref(false)
|
||||
@@ -162,21 +169,42 @@ type AuthProviderItem = {
|
||||
isLoading: Ref<boolean>
|
||||
}
|
||||
|
||||
const additonalLoginItems = computed(
|
||||
() => platform.auth.additionalLoginItems ?? []
|
||||
)
|
||||
let allowedAuthProviders: AuthProviderItem[] = []
|
||||
let additonalLoginItems: LoginItemDef[] = []
|
||||
|
||||
const doAdditionalLoginItemClickAction = async (item: LoginItemDef) => {
|
||||
await item.onClick()
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
|
||||
subscribeToStream(currentUser$, (user) => {
|
||||
if (user) hideModal()
|
||||
})
|
||||
|
||||
const res = await platform.auth.getAllowedAuthProviders()
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
toast.error(`${t("error.authproviders_load_error")}`)
|
||||
isLoadingAllowedAuthProviders.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// setup the normal auth providers
|
||||
const enabledAuthProviders = authProvidersAvailable.filter((provider) =>
|
||||
res.right.includes(provider.id)
|
||||
)
|
||||
allowedAuthProviders = enabledAuthProviders
|
||||
|
||||
// setup the additional login items
|
||||
additonalLoginItems =
|
||||
platform.auth.additionalLoginItems?.filter((item) =>
|
||||
res.right.includes(item.id)
|
||||
) ?? []
|
||||
|
||||
isLoadingAllowedAuthProviders.value = false
|
||||
})
|
||||
|
||||
const showLoginSuccess = () => {
|
||||
@@ -275,14 +303,7 @@ const signInWithEmail = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
mode.value = "sign-in"
|
||||
toast.clear()
|
||||
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
const authProviders: AuthProviderItem[] = [
|
||||
const authProvidersAvailable: AuthProviderItem[] = [
|
||||
{
|
||||
id: "GITHUB",
|
||||
icon: IconGithub,
|
||||
@@ -315,19 +336,10 @@ const authProviders: AuthProviderItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
// Do not format the `import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS` call into multiple lines!
|
||||
// prettier-ignore
|
||||
const allowedAuthProvidersIDsString =
|
||||
import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS
|
||||
const hideModal = () => {
|
||||
mode.value = "sign-in"
|
||||
toast.clear()
|
||||
|
||||
const allowedAuthProvidersIDs = allowedAuthProvidersIDsString
|
||||
? allowedAuthProvidersIDsString.split(",")
|
||||
: []
|
||||
|
||||
const allowedAuthProviders =
|
||||
allowedAuthProvidersIDs.length > 0
|
||||
? authProviders.filter((provider) =>
|
||||
allowedAuthProvidersIDs.includes(provider.id)
|
||||
)
|
||||
: authProviders
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ClientOptions } from "@urql/core"
|
||||
import { Observable } from "rxjs"
|
||||
import { Component } from "vue"
|
||||
import { getI18n } from "~/modules/i18n"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
/**
|
||||
* A common (and required) set of fields that describe a user.
|
||||
@@ -222,6 +223,11 @@ export type AuthPlatformDef = {
|
||||
*/
|
||||
setDisplayName: (name: string) => Promise<void>
|
||||
|
||||
/**
|
||||
* Returns the list of allowed auth providers for the platform ( the currently supported ones are GOOGLE, GITHUB, EMAIL, MICROSOFT, SAML )
|
||||
*/
|
||||
getAllowedAuthProviders: () => Promise<E.Either<string, string[]>>
|
||||
|
||||
/**
|
||||
* Defines the additional login items that should be shown in the login screen
|
||||
*/
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
"stream-browserify": "^3.0.0",
|
||||
"util": "^0.12.5",
|
||||
"vue": "^3.3.8",
|
||||
"workbox-window": "^7.0.0"
|
||||
"workbox-window": "^7.0.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/add": "^5.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHoppApp } from "@hoppscotch/common"
|
||||
import { def as authDef } from "./platform/auth"
|
||||
import { def as authDef } from "./platform/auth/auth.platform"
|
||||
import { def as environmentsDef } from "./platform/environments/environments.platform"
|
||||
import { def as collectionsDef } from "./platform/collections/collections.platform"
|
||||
import { def as settingsDef } from "./platform/settings/settings.platform"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import axios from "axios"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { z } from "zod"
|
||||
|
||||
const expectedAllowedProvidersSchema = z.object({
|
||||
// currently supported values are "GOOGLE", "GITHUB", "EMAIL", "MICROSOFT", "SAML"
|
||||
// keeping it as string to avoid backend accidentally breaking frontend when adding new providers
|
||||
providers: z.array(z.string()),
|
||||
})
|
||||
|
||||
export const getAllowedAuthProviders = async () => {
|
||||
try {
|
||||
const res = await axios.get(
|
||||
`${import.meta.env.VITE_BACKEND_API_URL}/auth/providers`,
|
||||
{
|
||||
withCredentials: true,
|
||||
}
|
||||
)
|
||||
|
||||
const parseResult = expectedAllowedProvidersSchema.safeParse(res.data)
|
||||
|
||||
if (!parseResult.success) {
|
||||
return E.left("SOMETHING_WENT_WRONG")
|
||||
}
|
||||
|
||||
return E.right(parseResult.data.providers)
|
||||
} catch (_) {
|
||||
return E.left("SOMETHING_WENT_WRONG")
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { PersistenceService } from "@hoppscotch/common/services/persistence"
|
||||
import axios from "axios"
|
||||
import { BehaviorSubject, Subject } from "rxjs"
|
||||
import { Ref, ref, watch } from "vue"
|
||||
import { getAllowedAuthProviders } from "./auth.api"
|
||||
|
||||
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
|
||||
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||
@@ -341,4 +342,5 @@ export const def: AuthPlatformDef = {
|
||||
window.location.href = "/"
|
||||
}
|
||||
},
|
||||
getAllowedAuthProviders,
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { authEvents$, def as platformAuth } from "@platform/auth"
|
||||
import { authEvents$, def as platformAuth } from "@platform/auth/auth.platform"
|
||||
import { CollectionsPlatformDef } from "@hoppscotch/common/platform/collections"
|
||||
import { runDispatchWithOutSyncing } from "../../lib/sync"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { authEvents$, def as platformAuth } from "@platform/auth"
|
||||
import { authEvents$, def as platformAuth } from "@platform/auth/auth.platform"
|
||||
import {
|
||||
createEnvironment,
|
||||
deleteEnvironment,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { authEvents$, def as platformAuth } from "@platform/auth"
|
||||
import { authEvents$, def as platformAuth } from "@platform/auth/auth.platform"
|
||||
import {
|
||||
restHistoryStore,
|
||||
RESTHistoryEntry,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SettingsPlatformDef } from "@hoppscotch/common/platform/settings"
|
||||
import { settingsSyncer } from "./settings.sync"
|
||||
import { authEvents$, def as platformAuth } from "@platform/auth"
|
||||
import { authEvents$, def as platformAuth } from "@platform/auth/auth.platform"
|
||||
import {
|
||||
createUserSettings,
|
||||
getUserSettings,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PersistableTabState } from "@hoppscotch/common/services/tab"
|
||||
import { HoppRESTDocument } from "@hoppscotch/common/helpers/rest/document"
|
||||
import { HoppUser } from "@hoppscotch/common/platform/auth"
|
||||
import { TabStatePlatformDef } from "@hoppscotch/common/platform/tab"
|
||||
import { def as platformAuth } from "@platform/auth"
|
||||
import { def as platformAuth } from "@platform/auth/auth.platform"
|
||||
import { getCurrentRestSession, updateUserSession } from "./tabState.api"
|
||||
import { SessionType } from "../../api/generated/graphql"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
Reference in New Issue
Block a user