feat: add local app login
This commit is contained in:
parent
b17375eb16
commit
892f69817b
7 changed files with 119 additions and 7 deletions
|
|
@ -192,6 +192,7 @@
|
||||||
"continue_with_github_enterprise": "Continue with GitHub Enterprise",
|
"continue_with_github_enterprise": "Continue with GitHub Enterprise",
|
||||||
"continue_with_google": "Continue with Google",
|
"continue_with_google": "Continue with Google",
|
||||||
"continue_with_microsoft": "Continue with Microsoft",
|
"continue_with_microsoft": "Continue with Microsoft",
|
||||||
|
"continue_with_username": "Continue with username",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"logged_out": "Logged out",
|
"logged_out": "Logged out",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
|
|
@ -200,7 +201,9 @@
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"re_enter_email": "Re-enter email",
|
"re_enter_email": "Re-enter email",
|
||||||
"send_magic_link": "Send a magic link",
|
"send_magic_link": "Send a magic link",
|
||||||
|
"password": "Password",
|
||||||
"sync": "Sync",
|
"sync": "Sync",
|
||||||
|
"username": "Username",
|
||||||
"we_sent_magic_link": "We sent you a magic link!",
|
"we_sent_magic_link": "We sent you a magic link!",
|
||||||
"we_sent_magic_link_description": "Check your inbox - we sent an email to {email}. It contains a magic link that will log you in."
|
"we_sent_magic_link_description": "Check your inbox - we sent an email to {email}. It contains a magic link that will log you in."
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,32 @@
|
||||||
:label="`${t('auth.send_magic_link')}`"
|
:label="`${t('auth.send_magic_link')}`"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
<form
|
||||||
|
v-if="mode === 'local'"
|
||||||
|
class="flex flex-col space-y-2"
|
||||||
|
@submit.prevent="signInWithUsernamePassword"
|
||||||
|
>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="form.username"
|
||||||
|
type="text"
|
||||||
|
placeholder=" "
|
||||||
|
:label="t('auth.username')"
|
||||||
|
input-styles="floating-input"
|
||||||
|
/>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
placeholder=" "
|
||||||
|
:label="t('auth.password')"
|
||||||
|
input-styles="floating-input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HoppButtonPrimary
|
||||||
|
:loading="signingInWithLocal"
|
||||||
|
type="submit"
|
||||||
|
:label="`${t('auth.login')}`"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!allowedAuthProviders?.length && !additionalLoginItems.length"
|
v-if="!allowedAuthProviders?.length && !additionalLoginItems.length"
|
||||||
|
|
@ -116,7 +142,7 @@
|
||||||
label="Privacy Policy"
|
label="Privacy Policy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mode === 'email'">
|
<div v-if="mode === 'email' || mode === 'local'">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('auth.all_sign_in_options')"
|
:label="t('auth.all_sign_in_options')"
|
||||||
:icon="IconArrowLeft"
|
:icon="IconArrowLeft"
|
||||||
|
|
@ -145,7 +171,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Ref, onMounted, ref } from "vue"
|
import { Component, Ref, onMounted, ref } from "vue"
|
||||||
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useStreamSubscriber } from "@composables/stream"
|
import { useStreamSubscriber } from "@composables/stream"
|
||||||
|
|
@ -159,6 +185,7 @@ import IconGoogle from "~icons/auth/google"
|
||||||
import IconMicrosoft from "~icons/auth/microsoft"
|
import IconMicrosoft from "~icons/auth/microsoft"
|
||||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||||
import IconFileText from "~icons/lucide/file-text"
|
import IconFileText from "~icons/lucide/file-text"
|
||||||
|
import IconKeyRound from "~icons/lucide/key-round"
|
||||||
|
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { LoginItemDef } from "~/platform/auth"
|
import { LoginItemDef } from "~/platform/auth"
|
||||||
|
|
@ -178,6 +205,8 @@ const persistenceService = useService(PersistenceService)
|
||||||
|
|
||||||
const form = {
|
const form = {
|
||||||
email: "",
|
email: "",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoadingAllowedAuthProviders = ref(true)
|
const isLoadingAllowedAuthProviders = ref(true)
|
||||||
|
|
@ -186,6 +215,7 @@ const signingInWithGoogle = ref(false)
|
||||||
const signingInWithGitHub = ref(false)
|
const signingInWithGitHub = ref(false)
|
||||||
const signingInWithMicrosoft = ref(false)
|
const signingInWithMicrosoft = ref(false)
|
||||||
const signingInWithEmail = ref(false)
|
const signingInWithEmail = ref(false)
|
||||||
|
const signingInWithLocal = ref(false)
|
||||||
const mode = ref("sign-in")
|
const mode = ref("sign-in")
|
||||||
|
|
||||||
const tosLink = import.meta.env.VITE_APP_TOS_LINK
|
const tosLink = import.meta.env.VITE_APP_TOS_LINK
|
||||||
|
|
@ -193,7 +223,7 @@ const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK
|
||||||
|
|
||||||
type AuthProviderItem = {
|
type AuthProviderItem = {
|
||||||
id: string
|
id: string
|
||||||
icon: typeof IconGithub
|
icon: Component
|
||||||
label: string
|
label: string
|
||||||
action: (...args: any[]) => any
|
action: (...args: any[]) => any
|
||||||
isLoading: Ref<boolean>
|
isLoading: Ref<boolean>
|
||||||
|
|
@ -356,6 +386,24 @@ const signInWithEmail = async () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const signInWithUsernamePassword = async () => {
|
||||||
|
signingInWithLocal.value = true
|
||||||
|
|
||||||
|
await platform.auth
|
||||||
|
.signInWithUsernamePassword(form.username, form.password)
|
||||||
|
.then(() => {
|
||||||
|
showLoginSuccess()
|
||||||
|
hideModal()
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
toast.error(`${t("error.something_went_wrong")}`)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
signingInWithLocal.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const authProvidersAvailable: AuthProviderItem[] = [
|
const authProvidersAvailable: AuthProviderItem[] = [
|
||||||
{
|
{
|
||||||
id: "GITHUB",
|
id: "GITHUB",
|
||||||
|
|
@ -395,6 +443,15 @@ const authProvidersAvailable: AuthProviderItem[] = [
|
||||||
},
|
},
|
||||||
isLoading: signingInWithEmail,
|
isLoading: signingInWithEmail,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "LOCAL",
|
||||||
|
icon: IconKeyRound,
|
||||||
|
label: t("auth.continue_with_username"),
|
||||||
|
action: () => {
|
||||||
|
mode.value = "local"
|
||||||
|
},
|
||||||
|
isLoading: signingInWithLocal,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,17 @@ export type AuthPlatformDef = {
|
||||||
*/
|
*/
|
||||||
signInWithEmail: (email: string) => Promise<void>
|
signInWithEmail: (email: string) => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to sign in user with username and password.
|
||||||
|
* @param username The username that is logging in.
|
||||||
|
* @param password The password for the local account.
|
||||||
|
* @returns An empty promise that is resolved when the operation is complete
|
||||||
|
*/
|
||||||
|
signInWithUsernamePassword: (
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
) => Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a given link is a valid sign in with email, magic link response url.
|
* 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)
|
* (i.e, a URL that COULD be from a magic link email)
|
||||||
|
|
@ -261,7 +272,7 @@ export type AuthPlatformDef = {
|
||||||
) => Promise<E.Either<GQLError<string>, undefined>>
|
) => Promise<E.Either<GQLError<string>, undefined>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of allowed auth providers for the platform ( the currently supported ones are GOOGLE, GITHUB, EMAIL, MICROSOFT, SAML )
|
* Returns the list of allowed auth providers for the platform ( the currently supported ones are GOOGLE, GITHUB, EMAIL, MICROSOFT, SAML, LOCAL )
|
||||||
*/
|
*/
|
||||||
getAllowedAuthProviders: () => Promise<E.Either<string, string[]>>
|
getAllowedAuthProviders: () => Promise<E.Either<string, string[]>>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
} from "@app/api/generated/graphql"
|
} from "@app/api/generated/graphql"
|
||||||
|
|
||||||
const expectedAllowedProvidersSchema = z.object({
|
const expectedAllowedProvidersSchema = z.object({
|
||||||
// currently supported values are "GOOGLE", "GITHUB", "EMAIL", "MICROSOFT", "SAML"
|
// currently supported values are "GOOGLE", "GITHUB", "EMAIL", "MICROSOFT", "SAML", "LOCAL"
|
||||||
// keeping it as string to avoid backend accidentally breaking frontend when adding new providers
|
// keeping it as string to avoid backend accidentally breaking frontend when adding new providers
|
||||||
providers: z.array(z.string()),
|
providers: z.array(z.string()),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -429,6 +429,25 @@ export const def: AuthPlatformDef = {
|
||||||
await sendMagicLink(email)
|
await sendMagicLink(email)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async signInWithUsernamePassword(username: string, password: string) {
|
||||||
|
const { response } = interceptorService.execute({
|
||||||
|
id: Date.now(),
|
||||||
|
url: `${import.meta.env.VITE_BACKEND_API_URL}/auth/local/signin`,
|
||||||
|
version: "HTTP/1.1",
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
content: content.json({ username, password }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await response
|
||||||
|
if (E.isLeft(res)) throw new Error("Failed to sign in")
|
||||||
|
|
||||||
|
await setAuthCookies(res.right.headers)
|
||||||
|
await setInitialUser()
|
||||||
|
},
|
||||||
|
|
||||||
async verifyEmailAddress() {
|
async verifyEmailAddress() {
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
} from "@app/api/generated/graphql"
|
} from "@app/api/generated/graphql"
|
||||||
|
|
||||||
const expectedAllowedProvidersSchema = z.object({
|
const expectedAllowedProvidersSchema = z.object({
|
||||||
// currently supported values are "GOOGLE", "GITHUB", "EMAIL", "MICROSOFT", "SAML"
|
// currently supported values are "GOOGLE", "GITHUB", "EMAIL", "MICROSOFT", "SAML", "LOCAL"
|
||||||
// keeping it as string to avoid backend accidentally breaking frontend when adding new providers
|
// keeping it as string to avoid backend accidentally breaking frontend when adding new providers
|
||||||
providers: z.array(z.string()),
|
providers: z.array(z.string()),
|
||||||
})
|
})
|
||||||
|
|
@ -35,6 +35,19 @@ export const getAllowedAuthProviders = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const signInLocal = async (username: string, password: string) => {
|
||||||
|
await axios.post(
|
||||||
|
`${import.meta.env.VITE_BACKEND_API_URL}/auth/local/signin`,
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const updateUserDisplayName = (updatedDisplayName: string) =>
|
export const updateUserDisplayName = (updatedDisplayName: string) =>
|
||||||
runMutation<
|
runMutation<
|
||||||
UpdateUserDisplayNameMutation,
|
UpdateUserDisplayNameMutation,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,11 @@ import {
|
||||||
} from "@hoppscotch/common/platform/auth"
|
} from "@hoppscotch/common/platform/auth"
|
||||||
import { PersistenceService } from "@hoppscotch/common/services/persistence"
|
import { PersistenceService } from "@hoppscotch/common/services/persistence"
|
||||||
|
|
||||||
import { getAllowedAuthProviders, updateUserDisplayName } from "./api"
|
import {
|
||||||
|
getAllowedAuthProviders,
|
||||||
|
signInLocal,
|
||||||
|
updateUserDisplayName,
|
||||||
|
} from "./api"
|
||||||
|
|
||||||
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
|
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
|
||||||
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||||
|
|
@ -276,6 +280,11 @@ export const def: AuthPlatformDef = {
|
||||||
await sendMagicLink(email)
|
await sendMagicLink(email)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async signInWithUsernamePassword(username: string, password: string) {
|
||||||
|
await signInLocal(username, password)
|
||||||
|
await setInitialUser()
|
||||||
|
},
|
||||||
|
|
||||||
isSignInWithEmailLink(url: string) {
|
isSignInWithEmailLink(url: string) {
|
||||||
const urlObject = new URL(url)
|
const urlObject = new URL(url)
|
||||||
const searchParams = new URLSearchParams(urlObject.search)
|
const searchParams = new URLSearchParams(urlObject.search)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue