diff --git a/packages/hoppscotch-sh-admin/locales/en.json b/packages/hoppscotch-sh-admin/locales/en.json index 6b137af03..0ddbc8e48 100644 --- a/packages/hoppscotch-sh-admin/locales/en.json +++ b/packages/hoppscotch-sh-admin/locales/en.json @@ -39,6 +39,7 @@ "delete_user_success": "User deleted successfully!!", "email": "Email", "email_failure": "Failed to send invitation", + "email_signin_failure": "Failed to login with Email", "email_success": "Email invitation sent successfully", "enter_team_email": "Please enter email of team owner!!", "error": "Something went wrong", @@ -50,6 +51,7 @@ "logout": "Logout", "magic_link_sign_in": "Click on the link to sign in.", "magic_link_success": "We sent a magic link to", + "microsoft_signin_failure": "Failed to login with Microsoft", "non_admin_logged_in": "Logged in as non admin user.", "non_admin_login": "You are logged in. But you're not an admin", "privacy_policy": "Privacy Policy", diff --git a/packages/hoppscotch-sh-admin/src/components.d.ts b/packages/hoppscotch-sh-admin/src/components.d.ts index 5c0e165ff..c31ea58fb 100644 --- a/packages/hoppscotch-sh-admin/src/components.d.ts +++ b/packages/hoppscotch-sh-admin/src/components.d.ts @@ -1,39 +1,40 @@ // 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'; +import '@vue/runtime-core' -export {}; +export {} declare module '@vue/runtime-core' { export interface GlobalComponents { - AppHeader: typeof import('./components/app/Header.vue')['default']; - AppLogin: typeof import('./components/app/Login.vue')['default']; - AppLogout: typeof import('./components/app/Logout.vue')['default']; - AppModal: typeof import('./components/app/Modal.vue')['default']; - AppSidebar: typeof import('./components/app/Sidebar.vue')['default']; - AppToast: typeof import('./components/app/Toast.vue')['default']; - DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default']; - HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']; - HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']; - HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']; - HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']; - HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']; - HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']; - HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']; - HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']; - HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']; - HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']; - IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']; - IconLucideInbox: typeof import('~icons/lucide/inbox')['default']; - TeamsAdd: typeof import('./components/teams/Add.vue')['default']; - TeamsDetails: typeof import('./components/teams/Details.vue')['default']; - TeamsInvite: typeof import('./components/teams/Invite.vue')['default']; - TeamsMembers: typeof import('./components/teams/Members.vue')['default']; - TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default']; - TeamsTable: typeof import('./components/teams/Table.vue')['default']; - Tippy: typeof import('vue-tippy')['Tippy']; - UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']; - UsersTable: typeof import('./components/users/Table.vue')['default']; + AppHeader: typeof import('./components/app/Header.vue')['default'] + AppLogin: typeof import('./components/app/Login.vue')['default'] + AppLogout: typeof import('./components/app/Logout.vue')['default'] + AppModal: typeof import('./components/app/Modal.vue')['default'] + AppSidebar: typeof import('./components/app/Sidebar.vue')['default'] + AppToast: typeof import('./components/app/Toast.vue')['default'] + DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default'] + HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'] + HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'] + HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'] + HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete'] + HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'] + HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'] + HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'] + HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'] + HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'] + HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'] + IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default'] + IconLucideInbox: typeof import('~icons/lucide/inbox')['default'] + TeamsAdd: typeof import('./components/teams/Add.vue')['default'] + TeamsDetails: typeof import('./components/teams/Details.vue')['default'] + TeamsInvite: typeof import('./components/teams/Invite.vue')['default'] + TeamsMembers: typeof import('./components/teams/Members.vue')['default'] + TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default'] + TeamsTable: typeof import('./components/teams/Table.vue')['default'] + Tippy: typeof import('vue-tippy')['Tippy'] + UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'] + UsersTable: typeof import('./components/users/Table.vue')['default'] } + } diff --git a/packages/hoppscotch-sh-admin/src/components/app/Header.vue b/packages/hoppscotch-sh-admin/src/components/app/Header.vue index b0280daff..54351cad6 100644 --- a/packages/hoppscotch-sh-admin/src/components/app/Header.vue +++ b/packages/hoppscotch-sh-admin/src/components/app/Header.vue @@ -89,8 +89,8 @@ const t = useI18n(); const { isOpen, isExpanded } = useSidebar(); const currentUser = useReadonlyStream( - auth.getProbableUserStream(), - auth.getProbableUser() + auth.getCurrentUserStream(), + auth.getCurrentUser() ); const expandSidebar = () => { diff --git a/packages/hoppscotch-sh-admin/src/components/app/Login.vue b/packages/hoppscotch-sh-admin/src/components/app/Login.vue index 71ac7a4a5..7621fcc63 100644 --- a/packages/hoppscotch-sh-admin/src/components/app/Login.vue +++ b/packages/hoppscotch-sh-admin/src/components/app/Login.vue @@ -184,91 +184,71 @@ onMounted(() => { subscribeToStream(currentUser$, (user) => { if (user && !user.isAdmin) { nonAdminUser.value = true; - toast.error(`${t('state.non_admin_login')}`); + toast.error(t('state.non_admin_login')); } }); }); -async function signInWithGoogle() { +const signInWithGoogle = () => { signingInWithGoogle.value = true; try { - await auth.signInUserWithGoogle(); + auth.signInUserWithGoogle(); } catch (e) { console.error(e); - /* - A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers - Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25 - */ - toast.error(`${t('state.google_signin_failure')}`); + toast.error(t('state.google_signin_failure')); } signingInWithGoogle.value = false; -} -async function signInWithGithub() { +}; + +const signInWithGithub = () => { signingInWithGitHub.value = true; try { - await auth.signInUserWithGithub(); + auth.signInUserWithGithub(); } catch (e) { console.error(e); - /* - A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers - Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25 - */ - toast.error(`${t('state.github_signin_failure')}`); + toast.error(t('state.github_signin_failure')); } signingInWithGitHub.value = false; -} +}; -async function signInWithMicrosoft() { +const signInWithMicrosoft = () => { signingInWithMicrosoft.value = true; try { - await auth.signInUserWithMicrosoft(); + auth.signInUserWithMicrosoft(); } catch (e) { console.error(e); - /* - A auth/account-exists-with-different-credential Firebase error wont happen between MS with Google or Github - If a Github account exists and user then logs in with MS email we get a "Something went wrong toast" and console errors and MS replaces GH as only provider. - The error messages are as follows: - FirebaseError: Firebase: Error (auth/popup-closed-by-user). - @firebase/auth: Auth (9.6.11): INTERNAL ASSERTION FAILED: Pending promise was never set - They may be related to https://github.com/firebase/firebaseui-web/issues/947 - */ - toast.error(`${t('state.error')}`); + toast.error(t('state.microsoft_signin_failure')); } signingInWithMicrosoft.value = false; -} -async function signInWithEmail() { - signingInWithEmail.value = true; +}; - await auth - .signInWithEmail(form.value.email) - .then(() => { - mode.value = 'email-sent'; - setLocalConfig('emailForSignIn', form.value.email); - }) - .catch((e: any) => { - console.error(e); - toast.error(e.message); - signingInWithEmail.value = false; - }) - .finally(() => { - signingInWithEmail.value = false; - }); -} +const signInWithEmail = async () => { + signingInWithEmail.value = true; + try { + await auth.signInWithEmail(form.value.email); + mode.value = 'email-sent'; + setLocalConfig('emailForSignIn', form.value.email); + } catch (e) { + console.error(e); + toast.error(t('state.email_signin_failure')); + } + signingInWithEmail.value = false; +}; const logout = async () => { try { await auth.signOutUser(); window.location.reload(); - toast.success(`${t('state.logged_out')}`); + toast.success(t('state.logged_out')); } catch (e) { console.error(e); - toast.error(`${t('state.error')}`); + toast.error(t('state.error')); } }; diff --git a/packages/hoppscotch-sh-admin/src/components/teams/Invite.vue b/packages/hoppscotch-sh-admin/src/components/teams/Invite.vue index 5f2bab5e9..380f6656c 100644 --- a/packages/hoppscotch-sh-admin/src/components/teams/Invite.vue +++ b/packages/hoppscotch-sh-admin/src/components/teams/Invite.vue @@ -200,7 +200,7 @@ import { } from '../../helpers/backend/graphql'; import { useToast } from '~/composables/toast'; import { useMutation, useQuery } from '@urql/vue'; -import { Email, EmailCodec } from '~/helpers/backend/Email'; +import { Email, EmailCodec } from '~/helpers/Email'; import IconTrash from '~icons/lucide/trash'; import IconPlus from '~icons/lucide/plus'; import IconCircleDot from '~icons/lucide/circle-dot'; diff --git a/packages/hoppscotch-sh-admin/src/composables/auth.ts b/packages/hoppscotch-sh-admin/src/composables/auth.ts deleted file mode 100644 index 4bbf12874..000000000 --- a/packages/hoppscotch-sh-admin/src/composables/auth.ts +++ /dev/null @@ -1,62 +0,0 @@ -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 - * is being updated to being logged in (fired multiple times), - * this is also called on component mount if the login - * was already resolved before mount. - */ -export function onLoggedIn(exec: (user: HoppUser) => void) { - const currentUser = useReadonlyStream( - platform.auth.getCurrentUserStream(), - platform.auth.getCurrentUser() - ); - - let watchStop: WatchStopHandle | null = null; - - onMounted(() => { - if (currentUser.value) exec(currentUser.value); - - watchStop = watch(currentUser, (newVal, prev) => { - if (prev === null && newVal !== null) { - exec(newVal); - } - }); - }); - - onBeforeUnmount(() => { - watchStop?.(); - }); -} - -/** - * A Vue composable function that calls its param function - * when a new event (login, logout etc.) happens in - * the auth system. - * - * NOTE: Unlike `onLoggedIn` for which the callback will be called once on mount with the current state, - * here the callback will only be called on authentication event occurances. - * You might want to check the auth state from an `onMounted` hook or something - * if you want to access the initial state - * - * @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(() => { - sub = authEvents$.subscribe((ev) => { - func(ev); - }); - }); - - onBeforeUnmount(() => { - sub?.unsubscribe(); - }); -} diff --git a/packages/hoppscotch-sh-admin/src/helpers/backend/Email.ts b/packages/hoppscotch-sh-admin/src/helpers/Email.ts similarity index 100% rename from packages/hoppscotch-sh-admin/src/helpers/backend/Email.ts rename to packages/hoppscotch-sh-admin/src/helpers/Email.ts diff --git a/packages/hoppscotch-sh-admin/src/helpers/auth.ts b/packages/hoppscotch-sh-admin/src/helpers/auth.ts index eaa430b82..f3641d127 100644 --- a/packages/hoppscotch-sh-admin/src/helpers/auth.ts +++ b/packages/hoppscotch-sh-admin/src/helpers/auth.ts @@ -1,12 +1,14 @@ -import axios from 'axios'; import { BehaviorSubject, Subject } from 'rxjs'; import { getLocalConfig, removeLocalConfig, setLocalConfig, } from './localpersistence'; -import { Ref, ref, watch } from 'vue'; +import { Ref, ref } from 'vue'; import * as O from 'fp-ts/Option'; +import authQuery from './backend/rest/authQuery'; +import { COOKIES_NOT_FOUND, UNAUTHORIZED } from './errors'; + /** * A common (and required) set of fields that describe a user. */ @@ -23,22 +25,16 @@ export type HoppUser = { /** 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; - + /** Flag to check for admin status */ isAdmin: 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 | { event: 'token_refresh' }; // We have previous login state, but the app is waiting for authentication @@ -51,17 +47,11 @@ export type GithubSignInResult = export const authEvents$ = new Subject< AuthEvent | { event: 'token_refresh' } >(); -const currentUser$ = new BehaviorSubject(null); -export const probableUser$ = new BehaviorSubject(null); -async function logout() { - await axios.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/logout`, { - withCredentials: true, - }); -} +const currentUser$ = new BehaviorSubject(null); const signOut = async (reloadWindow = false) => { - await logout(); + await authQuery.logout(); // Reload the window if both `access_token` and `refresh_token`is invalid // there by the user is taken to the login page @@ -69,7 +59,6 @@ const signOut = async (reloadWindow = false) => { window.location.reload(); } - probableUser$.next(null); currentUser$.next(null); removeLocalConfig('login_state'); @@ -78,142 +67,66 @@ const signOut = async (reloadWindow = false) => { }); }; -async function signInUserWithGithubFB() { - window.location.href = `${ - import.meta.env.VITE_BACKEND_API_URL - }/auth/github?redirect_uri=${import.meta.env.VITE_ADMIN_URL}`; -} - -async function signInUserWithGoogleFB() { - window.location.href = `${ - import.meta.env.VITE_BACKEND_API_URL - }/auth/google?redirect_uri=${import.meta.env.VITE_ADMIN_URL}`; -} - -async function signInUserWithMicrosoftFB() { - window.location.href = `${ - import.meta.env.VITE_BACKEND_API_URL - }/auth/microsoft?redirect_uri=${import.meta.env.VITE_ADMIN_URL}`; -} - -async function getInitialUserDetails() { - const res = await axios.post<{ - data?: { - me?: { - uid: string; - displayName: string; - email: string; - photoURL: string; - isAdmin: boolean; - createdOn: string; - // emailVerified: boolean - }; - }; - errors?: Array<{ - message: string; - }>; - }>( - `${import.meta.env.VITE_BACKEND_GQL_URL}`, - { - query: `query Me { - me { - uid - displayName - email - photoURL - isAdmin - createdOn - } - }`, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - withCredentials: true, - } - ); - +const getInitialUserDetails = async () => { + const res = await authQuery.getUserDetails(); return res.data; -} - +}; const isGettingInitialUser: Ref = ref(null); -function setUser(user: HoppUser | null) { +const setUser = (user: HoppUser | null) => { currentUser$.next(user); - probableUser$.next(user); - setLocalConfig('login_state', JSON.stringify(user)); -} +}; -async function setInitialUser() { +const setInitialUser = async () => { isGettingInitialUser.value = true; const res = await getInitialUserDetails(); - const error = res.errors && res.errors[0]; + if (res.errors?.[0]) { + const [error] = res.errors; - // no cookies sent. so the user is not logged in - if (error && error.message === 'auth/cookies_not_found') { - setUser(null); - isGettingInitialUser.value = false; - return; - } - - // cookies sent, but it is expired, we need to refresh the token - if (error && error.message === 'Unauthorized') { - const isRefreshSuccess = await refreshToken(); - - if (isRefreshSuccess) { - setInitialUser(); - } else { + if (error.message === COOKIES_NOT_FOUND) { setUser(null); - await signOut(true); - isGettingInitialUser.value = false; + } else if (error.message === UNAUTHORIZED) { + const isRefreshSuccess = await refreshToken(); + + if (isRefreshSuccess) { + setInitialUser(); + } else { + setUser(null); + signOut(true); + } } - - return; - } - - // no errors, we have a valid user - if (res.data && res.data.me) { - const hoppBackendUser = res.data.me; + } else if (res.data?.me) { + const { uid, displayName, email, photoURL, isAdmin } = res.data.me; const hoppUser: HoppUser = { - uid: hoppBackendUser.uid, - displayName: hoppBackendUser.displayName, - email: hoppBackendUser.email, - photoURL: hoppBackendUser.photoURL, - // all our signin methods currently guarantees the email is verified + uid, + displayName, + email, + photoURL, emailVerified: true, - isAdmin: hoppBackendUser.isAdmin, + isAdmin, }; if (!hoppUser.isAdmin) { - const isAdmin = await elevateUser(); - hoppUser.isAdmin = isAdmin; + hoppUser.isAdmin = await elevateUser(); } setUser(hoppUser); - isGettingInitialUser.value = false; - authEvents$.next({ event: 'login', user: hoppUser, }); - - return; } -} + + isGettingInitialUser.value = false; +}; const refreshToken = async () => { try { - const res = await axios.get( - `${import.meta.env.VITE_BACKEND_API_URL}/auth/refresh`, - { - withCredentials: true, - } - ); + const res = await authQuery.refreshToken(); authEvents$.next({ event: 'token_refresh', }); @@ -223,157 +136,67 @@ const refreshToken = async () => { } }; -async function elevateUser() { - const res = await axios.get( - `${import.meta.env.VITE_BACKEND_API_URL}/auth/verify/admin`, - { - withCredentials: true, - } - ); +const elevateUser = async () => { + const res = await authQuery.elevateUser(); + return Boolean(res.data?.isAdmin); +}; - return !!res.data?.isAdmin; -} - -async function sendMagicLink(email: string) { - const res = await axios.post( - `${import.meta.env.VITE_BACKEND_API_URL}/auth/signin?origin=admin`, - { - email, - }, - { - withCredentials: true, - } - ); - - if (res.data && res.data.deviceIdentifier) { - setLocalConfig('deviceIdentifier', res.data.deviceIdentifier); - } else { +const sendMagicLink = async (email: string) => { + const res = await authQuery.sendMagicLink(email); + if (!res.data?.deviceIdentifier) { throw new Error('test: does not get device identifier'); } - + setLocalConfig('deviceIdentifier', res.data.deviceIdentifier); return res.data; -} +}; export const auth = { getCurrentUserStream: () => currentUser$, getAuthEventsStream: () => authEvents$, - getProbableUserStream: () => probableUser$, - getCurrentUser: () => currentUser$.value, - getProbableUser: () => probableUser$.value, - getBackendHeaders() { - return {}; - }, - getGQLClientOptions() { - return { - fetchOptions: { - credentials: 'include', - }, - }; + performAuthInit: () => { + const currentUser = JSON.parse(getLocalConfig('login_state') ?? 'null'); + currentUser$.next(currentUser); + return setInitialUser(); }, - /** - * it is not possible for us to know if the current cookie is expired because we cannot access http-only cookies from js - * hence just returning if the currentUser$ has a value associated with it - */ - willBackendHaveAuthError() { - return !currentUser$.value; - }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - onBackendGQLClientShouldReconnect(func: () => void) { - authEvents$.subscribe((event) => { - if ( - event.event == 'login' || - event.event == 'logout' || - event.event == 'token_refresh' - ) { - func(); - } - }); - }, + signInWithEmail: (email: string) => sendMagicLink(email), - /** - * we cannot access our auth cookies from javascript, so leaving this as null - */ - getDevOptsBackendIDToken() { - return null; - }, - async performAuthInit() { - const probableUser = JSON.parse(getLocalConfig('login_state') ?? 'null'); - probableUser$.next(probableUser); - await setInitialUser(); - }, - - waitProbableLoginToConfirm() { - return new Promise((resolve, reject) => { - if (this.getCurrentUser()) { - resolve(); - } - - if (!probableUser$.value) reject(new Error('no_probable_user')); - - const unwatch = watch(isGettingInitialUser, (val) => { - if (val === true || val === false) { - resolve(); - unwatch(); - } - }); - }); - }, - - async signInWithEmail(email: string) { - await sendMagicLink(email); - }, - - isSignInWithEmailLink(url: string) { + isSignInWithEmailLink: (url: string) => { const urlObject = new URL(url); const searchParams = new URLSearchParams(urlObject.search); - - return !!searchParams.get('token'); + return Boolean(searchParams.get('token')); }, - async verifyEmailAddress() { - return; + signInUserWithGoogle: () => { + window.location.href = `${ + import.meta.env.VITE_BACKEND_API_URL + }/auth/google?redirect_uri=${import.meta.env.VITE_ADMIN_URL}`; }, - async signInUserWithGoogle() { - await signInUserWithGoogleFB(); + + signInUserWithGithub: () => { + window.location.href = `${ + import.meta.env.VITE_BACKEND_API_URL + }/auth/github?redirect_uri=${import.meta.env.VITE_ADMIN_URL}`; }, - async signInUserWithGithub() { - await signInUserWithGithubFB(); - return undefined; + + signInUserWithMicrosoft: () => { + window.location.href = `${ + import.meta.env.VITE_BACKEND_API_URL + }/auth/microsoft?redirect_uri=${import.meta.env.VITE_ADMIN_URL}`; }, - async signInUserWithMicrosoft() { - await signInUserWithMicrosoftFB(); - }, - async signInWithEmailLink(email: string, url: string) { + + signInWithEmailLink: (url: string) => { const urlObject = new URL(url); const searchParams = new URLSearchParams(urlObject.search); - const token = searchParams.get('token'); const deviceIdentifier = getLocalConfig('deviceIdentifier'); - await axios.post( - `${import.meta.env.VITE_BACKEND_API_URL}/auth/verify`, - { - token: token, - deviceIdentifier, - }, - { - withCredentials: true, - } - ); - }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async setEmailAddress(_email: string) { - return; - }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async setDisplayName(name: string) { - return; + return authQuery.signInWithEmailLink(token, deviceIdentifier); }, - async performAuthRefresh() { + performAuthRefresh: async () => { const isRefreshSuccess = await refreshToken(); if (isRefreshSuccess) { @@ -386,12 +209,10 @@ export const auth = { } }, - async signOutUser(reloadWindow = false) { - await signOut(reloadWindow); - }, + signOutUser: (reloadWindow = false) => signOut(reloadWindow), - async processMagicLink() { - if (this.isSignInWithEmailLink(window.location.href)) { + processMagicLink: async () => { + if (auth.isSignInWithEmailLink(window.location.href)) { const deviceIdentifier = getLocalConfig('deviceIdentifier'); if (!deviceIdentifier) { @@ -400,7 +221,7 @@ export const auth = { ); } - await this.signInWithEmailLink(deviceIdentifier, window.location.href); + await auth.signInWithEmailLink(window.location.href); removeLocalConfig('deviceIdentifier'); window.location.href = import.meta.env.VITE_ADMIN_URL; diff --git a/packages/hoppscotch-sh-admin/src/helpers/axiosConfig.ts b/packages/hoppscotch-sh-admin/src/helpers/axiosConfig.ts new file mode 100644 index 000000000..533ea35d1 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/helpers/axiosConfig.ts @@ -0,0 +1,20 @@ +import axios from 'axios'; + +const baseConfig = { + headers: { + 'Content-type': 'application/json', + }, + withCredentials: true, +}; + +const gqlApi = axios.create({ + ...baseConfig, + baseURL: import.meta.env.VITE_BACKEND_GQL_URL, +}); + +const restApi = axios.create({ + ...baseConfig, + baseURL: import.meta.env.VITE_BACKEND_API_URL, +}); + +export { gqlApi, restApi }; diff --git a/packages/hoppscotch-sh-admin/src/helpers/backend/rest/authQuery.ts b/packages/hoppscotch-sh-admin/src/helpers/backend/rest/authQuery.ts new file mode 100644 index 000000000..176d9eaf2 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/helpers/backend/rest/authQuery.ts @@ -0,0 +1,32 @@ +import { gqlApi, restApi } from '~/helpers/axiosConfig'; + +export default { + getUserDetails: () => + gqlApi.post('', { + query: `query Me { + me { + uid + displayName + email + photoURL + isAdmin + createdOn + } + }`, + }), + refreshToken: () => restApi.get('/auth/refresh'), + elevateUser: () => restApi.get('/auth/verify/admin'), + sendMagicLink: (email: string) => + restApi.post('/auth/signin?origin=admin', { + email, + }), + signInWithEmailLink: ( + token: string | null, + deviceIdentifier: string | null + ) => + restApi.post('/auth/verify', { + token, + deviceIdentifier, + }), + logout: () => restApi.get('/auth/logout'), +}; diff --git a/packages/hoppscotch-sh-admin/src/helpers/error.ts b/packages/hoppscotch-sh-admin/src/helpers/error.ts deleted file mode 100644 index f91956a6a..000000000 --- a/packages/hoppscotch-sh-admin/src/helpers/error.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const throwError = (message: string): never => { - throw new Error(message) -} diff --git a/packages/hoppscotch-sh-admin/src/helpers/errors.ts b/packages/hoppscotch-sh-admin/src/helpers/errors.ts new file mode 100644 index 000000000..5473ec528 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/helpers/errors.ts @@ -0,0 +1,9 @@ +/* No cookies were found in the auth request + * (AuthService) + */ +export const COOKIES_NOT_FOUND = 'auth/cookies_not_found' as const; + +export const UNAUTHORIZED = 'Unauthorized' as const; + +// Sometimes the backend returns Unauthorized error message as follows: +export const GRAPHQL_UNAUTHORIZED = '[GraphQL] Unauthorized' as const; diff --git a/packages/hoppscotch-sh-admin/src/main.ts b/packages/hoppscotch-sh-admin/src/main.ts index 18abd22e1..cdba5cc3f 100644 --- a/packages/hoppscotch-sh-admin/src/main.ts +++ b/packages/hoppscotch-sh-admin/src/main.ts @@ -16,6 +16,7 @@ import { HOPP_MODULES } from './modules'; import { auth } from './helpers/auth'; import { pipe } from 'fp-ts/function'; import * as O from 'fp-ts/Option'; +import { GRAPHQL_UNAUTHORIZED } from './helpers/errors'; // Top-level await is not available in our targets (async () => { @@ -40,12 +41,12 @@ import * as O from 'fp-ts/Option'; async refreshAuth() { pipe( await auth.performAuthRefresh(), - O.getOrElseW(async () => await auth.signOutUser(true)) + O.getOrElseW(() => auth.signOutUser(true)) ); }, didAuthError(error, _operation) { - return error.message === '[GraphQL] Unauthorized'; + return error.message === GRAPHQL_UNAUTHORIZED; }, }; }), diff --git a/packages/hoppscotch-sh-admin/src/pages/enter.vue b/packages/hoppscotch-sh-admin/src/pages/enter.vue index 0ba829092..7cef95526 100644 --- a/packages/hoppscotch-sh-admin/src/pages/enter.vue +++ b/packages/hoppscotch-sh-admin/src/pages/enter.vue @@ -13,8 +13,8 @@ import { auth } from '~/helpers/auth'; const signingInWithEmail = ref(false); const error = ref(null); -onBeforeMount(() => { - auth.performAuthInit(); +onBeforeMount(async () => { + await auth.performAuthInit(); }); onMounted(async () => {