diff --git a/packages/hoppscotch-sh-admin/assets/images/hoppscotch-title.svg b/packages/hoppscotch-sh-admin/assets/images/hoppscotch-title.svg new file mode 100644 index 00000000..b33ed65b --- /dev/null +++ b/packages/hoppscotch-sh-admin/assets/images/hoppscotch-title.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/packages/hoppscotch-sh-admin/locales/en.json b/packages/hoppscotch-sh-admin/locales/en.json index b95ca905..c2c4b156 100644 --- a/packages/hoppscotch-sh-admin/locales/en.json +++ b/packages/hoppscotch-sh-admin/locales/en.json @@ -1,10 +1,12 @@ { "app": { "collapse_sidebar": "Collapse Sidebar", + "continue_to_dashboard": "Continue to Dashboard", "expand_sidebar": "Expand Sidebar", "name": "HOPPSCOTCH", "no_name": "No name", - "open_navigation": "Open Navigation" + "open_navigation": "Open Navigation", + "read_documentation": "Read Documentation" }, "configs": { "auth_providers": { @@ -16,6 +18,15 @@ }, "confirm_changes": "Hoppscotch server must restart to reflect the new changes. Confirm changes made to the server configurations?", "input_empty": "Please fill all the fields before updating the configurations", + "data_sharing": { + "title": "Data Sharing", + "description": "Help improve Hoppscotch by sharing anonymous data", + "enable": "Enable Data Sharing", + "secondary_title": "Data Sharing Configurations", + "see_shared": "See what is shared", + "toggle_description": "Share anonymous data", + "update_failure": "Failed to update data sharing configurations!!" + }, "load_error": "Unable to load server configurations", "mail_configs": { "description": " Configure the smtp configurations", @@ -40,6 +51,14 @@ "save_changes": "Save Changes", "title": "Configurations" }, + "data_sharing": { + "description": "Share anonymous data usage to improve Hoppscotch", + "enable": "Enable Data Sharing", + "see_shared": "See what is shared", + "toggle_description": "Share data and make Hoppscotch better", + "title": "Make Hoppscotch Better", + "welcome": "Welcome to" + }, "metrics": { "dashboard": "Dashboard", "no_metrics": "No metrics found", @@ -48,6 +67,13 @@ "total_teams": "Total Workspaces", "total_users": "Total Users" }, + "newsletter": { + "description": "Get updates about our latest news", + "subscribe": "Subscribe", + "title": "Stay in Touch", + "toggle_description": "Get updates about the latest at Hoppscotch", + "unsubscribe": "Unsubscribe" + }, "settings": { "settings": "Settings" }, @@ -86,6 +112,7 @@ "copied_to_clipboard": "Copied to clipboard", "create_team_failure": "Failed to create workspace!!", "create_team_success": "Workspace created successfully!!", + "data_sharing_failure": "Failed to update data sharing settings", "delete_request_failure": "Shared Request deletion failed!!", "delete_request_success": "Shared Request deleted successfully!!", "delete_team_failure": "Workspace deletion failed!!", @@ -108,6 +135,7 @@ "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", + "newsletter_failure": "Unable to update newsletter settings", "non_admin_logged_in": "Logged in as non admin user.", "non_admin_login": "You are logged in. But you're not an admin", "owner_not_present": "Atleast one owner should be present in the team!!", @@ -127,10 +155,12 @@ "role_update_success": "Roles updated successfully!!", "self_host_docs": "Self Host Documentation", "send_magic_link": "Send magic link", + "setup_failure": "Setup has failed!!", + "setup_success": "Setup completed successfully!!", "sign_in_agreement": "By signing in, you are agreeing to our", "sign_in_options": "All sign in option", "sign_out": "Sign out", - "team_name_long": "Workspace name should be atleast 6 characters long!!", + "team_name_too_short": "Workspace name should be atleast 6 characters long!!", "user_not_found": "User not found in the infra!!" }, "teams": { diff --git a/packages/hoppscotch-sh-admin/src/components.d.ts b/packages/hoppscotch-sh-admin/src/components.d.ts index 8deeb2b8..17e02a1c 100644 --- a/packages/hoppscotch-sh-admin/src/components.d.ts +++ b/packages/hoppscotch-sh-admin/src/components.d.ts @@ -1,54 +1,47 @@ // 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']; - HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']; - HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']; - HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']; - HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']; - HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable']; - HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']; - HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']; - IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']; - IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']; - IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']; - IconLucideInbox: typeof import('~icons/lucide/inbox')['default']; - IconLucideUser: typeof import('~icons/lucide/user')['default']; - SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default']; - SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default']; - SettingsReset: typeof import('./components/settings/Reset.vue')['default']; - SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default']; - SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.vue')['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']; - Tippy: typeof import('vue-tippy')['Tippy']; - UiAutoResetIcon: typeof import('./components/ui/AutoResetIcon.vue')['default']; - UsersDetails: typeof import('./components/users/Details.vue')['default']; - UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']; - UsersSharedRequests: typeof import('./components/users/SharedRequests.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'] + HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'] + HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'] + HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'] + HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink'] + HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'] + HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'] + HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle'] + IconLucideInbox: typeof import('~icons/lucide/inbox')['default'] + SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default'] + SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default'] + SettingsDataSharing: typeof import('./components/settings/DataSharing.vue')['default'] + SettingsReset: typeof import('./components/settings/Reset.vue')['default'] + SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default'] + SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.vue')['default'] + SetupDataSharingAndNewsletter: typeof import('./components/setup/DataSharingAndNewsletter.vue')['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'] + Tippy: typeof import('vue-tippy')['Tippy'] + UiAutoResetIcon: typeof import('./components/ui/AutoResetIcon.vue')['default'] + UsersDetails: typeof import('./components/users/Details.vue')['default'] + UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'] + UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default'] } + } diff --git a/packages/hoppscotch-sh-admin/src/components/settings/Configurations.vue b/packages/hoppscotch-sh-admin/src/components/settings/Configurations.vue index bd20a094..c1267fcb 100644 --- a/packages/hoppscotch-sh-admin/src/components/settings/Configurations.vue +++ b/packages/hoppscotch-sh-admin/src/components/settings/Configurations.vue @@ -2,6 +2,7 @@
+
diff --git a/packages/hoppscotch-sh-admin/src/components/settings/DataSharing.vue b/packages/hoppscotch-sh-admin/src/components/settings/DataSharing.vue new file mode 100644 index 00000000..98367302 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/components/settings/DataSharing.vue @@ -0,0 +1,66 @@ + + + diff --git a/packages/hoppscotch-sh-admin/src/components/settings/ServerRestart.vue b/packages/hoppscotch-sh-admin/src/components/settings/ServerRestart.vue index 8d27964c..12dd3c62 100644 --- a/packages/hoppscotch-sh-admin/src/components/settings/ServerRestart.vue +++ b/packages/hoppscotch-sh-admin/src/components/settings/ServerRestart.vue @@ -22,6 +22,7 @@ import { EnableAndDisableSsoDocument, ResetInfraConfigsDocument, UpdateInfraConfigsDocument, + ToggleAnalyticsCollectionDocument, } from '~/helpers/backend/graphql'; const t = useI18n(); @@ -43,10 +44,17 @@ const updateInfraConfigsMutation = useMutation(UpdateInfraConfigsDocument); const updateAllowedAuthProviderMutation = useMutation( EnableAndDisableSsoDocument ); +const toggleDataSharingMutation = useMutation( + ToggleAnalyticsCollectionDocument +); // Mutation handlers -const { updateInfraConfigs, updateAuthProvider, resetInfraConfigs } = - useConfigHandler(props.workingConfigs); +const { + updateInfraConfigs, + updateAuthProvider, + resetInfraConfigs, + updateDataSharingConfigs, +} = useConfigHandler(props.workingConfigs); // Call relevant mutations on component mount and initiate server restart const duration = ref(30); @@ -80,6 +88,12 @@ onMounted(async () => { updateAllowedAuthProviderMutation ); if (!authResult) return; + + const dataSharingResult = await updateDataSharingConfigs( + toggleDataSharingMutation + ); + + if (!dataSharingResult) return; } restart.value = true; diff --git a/packages/hoppscotch-sh-admin/src/components/setup/DataSharingAndNewsletter.vue b/packages/hoppscotch-sh-admin/src/components/setup/DataSharingAndNewsletter.vue new file mode 100644 index 00000000..443acf3f --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/components/setup/DataSharingAndNewsletter.vue @@ -0,0 +1,141 @@ + + + diff --git a/packages/hoppscotch-sh-admin/src/composables/useClientHandler.ts b/packages/hoppscotch-sh-admin/src/composables/useClientHandler.ts index 5a1ba0a1..4176e598 100644 --- a/packages/hoppscotch-sh-admin/src/composables/useClientHandler.ts +++ b/packages/hoppscotch-sh-admin/src/composables/useClientHandler.ts @@ -25,22 +25,26 @@ export function useClientHandler< const fetchData = async () => { fetching.value = true; - try { - const result = await client - .query(query, { - ...variables, - }) - .toPromise(); - if (getList) { - const resultList = getList(result.data!); - dataAsList.value.push(...resultList); - } else { - data.value = result.data; - } - } catch (e) { + const result = await client + .query(query, { + ...variables, + }) + .toPromise(); + + if (result.error) { error.value = true; + fetching.value = false; + return; } + + if (getList) { + const resultList = getList(result.data!); + dataAsList.value.push(...resultList); + } else { + data.value = result.data; + } + fetching.value = false; }; diff --git a/packages/hoppscotch-sh-admin/src/composables/useConfigHandler.ts b/packages/hoppscotch-sh-admin/src/composables/useConfigHandler.ts index 59aa6f7d..011ab369 100644 --- a/packages/hoppscotch-sh-admin/src/composables/useConfigHandler.ts +++ b/packages/hoppscotch-sh-admin/src/composables/useConfigHandler.ts @@ -1,19 +1,20 @@ -import { computed, onMounted, ref } from 'vue'; +import { AnyVariables, UseMutationResponse } from '@urql/vue'; import { cloneDeep } from 'lodash-es'; -import { UseMutationResponse } from '@urql/vue'; -import { useClientHandler } from './useClientHandler'; -import { useToast } from './toast'; +import { computed, onMounted, ref } from 'vue'; import { useI18n } from '~/composables/i18n'; import { + AllowedAuthProvidersDocument, + EnableAndDisableSsoArgs, + EnableAndDisableSsoMutation, + InfraConfigArgs, InfraConfigEnum, InfraConfigsDocument, - AllowedAuthProvidersDocument, - EnableAndDisableSsoMutation, - UpdateInfraConfigsMutation, ResetInfraConfigsMutation, - EnableAndDisableSsoArgs, - InfraConfigArgs, + ToggleAnalyticsCollectionMutation, + UpdateInfraConfigsMutation, } from '~/helpers/backend/graphql'; +import { useToast } from './toast'; +import { useClientHandler } from './useClientHandler'; // Types export type SsoAuthProviders = 'google' | 'microsoft' | 'github'; @@ -54,6 +55,11 @@ export type Config = { mailer_from_address: string; }; }; + + dataSharingConfigs: { + name: string; + enabled: boolean; + }; }; type UpdatedConfigs = { @@ -86,6 +92,7 @@ export function useConfigHandler(updatedConfigs?: Config) { 'GITHUB_CLIENT_SECRET', 'MAILER_SMTP_URL', 'MAILER_ADDRESS_FROM', + 'ALLOW_ANALYTICS_COLLECTION', ] as InfraConfigEnum[], }, (x) => x.infraConfigs @@ -164,6 +171,12 @@ export function useConfigHandler(updatedConfigs?: Config) { ?.value ?? '', }, }, + dataSharingConfigs: { + name: 'data_sharing', + enabled: !!infraConfigs.value.find( + (x) => x.name === 'ALLOW_ANALYTICS_COLLECTION' && x.value === 'true' + ), + }, }; // Cloning the current configs to working configs @@ -262,15 +275,23 @@ export function useConfigHandler(updatedConfigs?: Config) { // Checking if any of the config fields are empty const isFieldEmpty = (field: string) => field.trim() === ''; - const AreAnyConfigFieldsEmpty = (config: Config): boolean => { - const providerFieldsEmpty = Object.values(config.providers).some( - (provider) => Object.values(provider.fields).some(isFieldEmpty) - ); - const mailFieldsEmpty = Object.values(config.mailConfigs.fields).some( - isFieldEmpty - ); + type ConfigSection = { + enabled: boolean; + fields: Record; + }; - return providerFieldsEmpty || mailFieldsEmpty; + const AreAnyConfigFieldsEmpty = (config: Config): boolean => { + const sections: Array = [ + config.providers.github, + config.providers.google, + config.providers.microsoft, + config.mailConfigs, + ]; + + return sections.some( + (section) => + section.enabled && Object.values(section.fields).some(isFieldEmpty) + ); }; // Transforming the working configs back into the format required by the mutations @@ -297,55 +318,70 @@ export function useConfigHandler(updatedConfigs?: Config) { ]; }); - // Updating the auth provider configurations - const updateAuthProvider = async ( - updateProviderStatus: UseMutationResponse - ) => { - const variables = { - providerInfo: - updatedAllowedAuthProviders.value as EnableAndDisableSsoArgs[], - }; - - const result = await updateProviderStatus.executeMutation(variables); + // Generic function to handle mutation execution and error handling + const executeMutation = async ( + mutation: UseMutationResponse, + variables: AnyVariables = undefined, + errorMessage: string + ): Promise => { + const result = await mutation.executeMutation(variables); if (result.error) { - toast.error(t('configs.auth_providers.update_failure')); + toast.error(t(errorMessage)); return false; } return true; }; + // Updating the auth provider configurations + const updateAuthProvider = ( + updateProviderStatus: UseMutationResponse + ) => + executeMutation( + updateProviderStatus, + { + providerInfo: + updatedAllowedAuthProviders.value as EnableAndDisableSsoArgs[], + }, + 'configs.auth_providers.update_failure' + ); + // Updating the infra configurations - const updateInfraConfigs = async ( + const updateInfraConfigs = ( updateInfraConfigsMutation: UseMutationResponse - ) => { - const variables = { - infraConfigs: updatedInfraConfigs.value as InfraConfigArgs[], - }; - - const result = await updateInfraConfigsMutation.executeMutation(variables); - - if (result.error) { - toast.error(t('configs.mail_configs.update_failure')); - return false; - } - - return true; - }; + ) => + executeMutation( + updateInfraConfigsMutation, + { + infraConfigs: updatedInfraConfigs.value as InfraConfigArgs[], + }, + 'configs.mail_configs.update_failure' + ); // Resetting the infra configurations - const resetInfraConfigs = async ( + const resetInfraConfigs = ( resetInfraConfigsMutation: UseMutationResponse - ) => { - const result = await resetInfraConfigsMutation.executeMutation(); + ) => + executeMutation( + resetInfraConfigsMutation, + undefined, + 'configs.reset.failure' + ); - if (result.error) { - toast.error(t('configs.reset.failure')); - return false; - } - return true; - }; + // Updating the data sharing configurations + const updateDataSharingConfigs = ( + toggleDataSharingMutation: UseMutationResponse + ) => + executeMutation( + toggleDataSharingMutation, + { + status: updatedConfigs?.dataSharingConfigs.enabled + ? 'ENABLE' + : 'DISABLE', + }, + 'configs.data_sharing.update_failure' + ); return { currentConfigs, @@ -353,6 +389,7 @@ export function useConfigHandler(updatedConfigs?: Config) { updatedInfraConfigs, updatedAllowedAuthProviders, updateAuthProvider, + updateDataSharingConfigs, updateInfraConfigs, resetInfraConfigs, fetchingInfraConfigs, diff --git a/packages/hoppscotch-sh-admin/src/composables/usePagedQuery.ts b/packages/hoppscotch-sh-admin/src/composables/usePagedQuery.ts index 450749eb..67665529 100644 --- a/packages/hoppscotch-sh-admin/src/composables/usePagedQuery.ts +++ b/packages/hoppscotch-sh-admin/src/composables/usePagedQuery.ts @@ -1,4 +1,4 @@ -import { onMounted, ref } from 'vue'; +import { Ref, onMounted, ref } from 'vue'; import { DocumentNode } from 'graphql'; import { TypedDocumentNode, useClientHandle } from '@urql/vue'; @@ -16,38 +16,41 @@ export function usePagedQuery< const { client } = useClientHandle(); const fetching = ref(true); const error = ref(false); - const list = ref([]); + const list: Ref = ref([]); const currentPage = ref(0); const hasNextPage = ref(true); const fetchNextPage = async () => { fetching.value = true; - try { - const cursor = - list.value.length > 0 ? getCursor(list.value.at(-1)) : undefined; - const variablesForPagination = { - ...variables, - take: itemsPerPage, - cursor, - }; + const cursor = + list.value.length > 0 ? getCursor(list.value.at(-1)!) : undefined; + const variablesForPagination = { + ...variables, + take: itemsPerPage, + cursor, + }; - const result = await client - .query(query, variablesForPagination) - .toPromise(); - const resultList = getList(result.data!); + const result = await client + .query(query, variablesForPagination) + .toPromise(); - if (resultList.length < itemsPerPage) { - hasNextPage.value = false; - } - - list.value.push(...resultList); - currentPage.value++; - } catch (e) { + if (result.error) { error.value = true; - } finally { fetching.value = false; + return; } + + const resultList = getList(result.data!); + + if (resultList.length < itemsPerPage) { + hasNextPage.value = false; + } + + list.value.push(...resultList); + currentPage.value++; + + fetching.value = false; }; onMounted(async () => { diff --git a/packages/hoppscotch-sh-admin/src/helpers/auth.ts b/packages/hoppscotch-sh-admin/src/helpers/auth.ts index 64251a49..b1ea2c69 100644 --- a/packages/hoppscotch-sh-admin/src/helpers/auth.ts +++ b/packages/hoppscotch-sh-admin/src/helpers/auth.ts @@ -67,7 +67,7 @@ const signOut = async (reloadWindow = false) => { }); }; -const getInitialUserDetails = async () => { +const getUserDetails = async () => { const res = await authQuery.getUserDetails(); return res.data; }; @@ -80,7 +80,7 @@ const setUser = (user: HoppUser | null) => { const setInitialUser = async () => { isGettingInitialUser.value = true; - const res = await getInitialUserDetails(); + const res = await getUserDetails(); if (res.errors?.[0]) { const [error] = res.errors; @@ -154,7 +154,7 @@ export const auth = { getCurrentUserStream: () => currentUser$, getAuthEventsStream: () => authEvents$, getCurrentUser: () => currentUser$.value, - + getUserDetails, performAuthInit: () => { const currentUser = JSON.parse(getLocalConfig('login_state') ?? 'null'); currentUser$.next(currentUser); @@ -232,4 +232,24 @@ export const auth = { const res = await authQuery.getProviders(); return res.data?.providers; }, + + getFirstTimeInfraSetupStatus: async (): Promise => { + try { + const res = await authQuery.getFirstTimeInfraSetupStatus(); + return res.data?.value === 'true'; + } catch (err) { + // Setup is not done + return true; + } + }, + + updateFirstTimeInfraSetupStatus: async () => { + try { + await authQuery.updateFirstTimeInfraSetupStatus(); + return true; + } catch (err) { + console.error(err); + return false; + } + }, }; diff --git a/packages/hoppscotch-sh-admin/src/helpers/axiosConfig.ts b/packages/hoppscotch-sh-admin/src/helpers/axiosConfig.ts index 533ea35d..a250d8c7 100644 --- a/packages/hoppscotch-sh-admin/src/helpers/axiosConfig.ts +++ b/packages/hoppscotch-sh-admin/src/helpers/axiosConfig.ts @@ -17,4 +17,10 @@ const restApi = axios.create({ baseURL: import.meta.env.VITE_BACKEND_API_URL, }); -export { gqlApi, restApi }; +const listmonkApi = axios.create({ + ...baseConfig, + withCredentials: false, + baseURL: 'https://listmonk.hoppscotch.com/api/public', +}); + +export { gqlApi, restApi, listmonkApi }; diff --git a/packages/hoppscotch-sh-admin/src/helpers/backend/gql/mutations/ToggleAnalyticsCollection.graphql b/packages/hoppscotch-sh-admin/src/helpers/backend/gql/mutations/ToggleAnalyticsCollection.graphql new file mode 100644 index 00000000..06966203 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/helpers/backend/gql/mutations/ToggleAnalyticsCollection.graphql @@ -0,0 +1,3 @@ +mutation ToggleAnalyticsCollection($status: ServiceStatus!) { + toggleAnalyticsCollection(status: $status) +} diff --git a/packages/hoppscotch-sh-admin/src/helpers/backend/rest/authQuery.ts b/packages/hoppscotch-sh-admin/src/helpers/backend/rest/authQuery.ts index d6fc35a9..5ab3023b 100644 --- a/packages/hoppscotch-sh-admin/src/helpers/backend/rest/authQuery.ts +++ b/packages/hoppscotch-sh-admin/src/helpers/backend/rest/authQuery.ts @@ -29,5 +29,7 @@ export default { token, deviceIdentifier, }), + getFirstTimeInfraSetupStatus: () => restApi.get('/site/setup'), + updateFirstTimeInfraSetupStatus: () => restApi.put('/site/setup'), logout: () => restApi.get('/auth/logout'), }; diff --git a/packages/hoppscotch-sh-admin/src/modules/admin.ts b/packages/hoppscotch-sh-admin/src/modules/admin.ts index 03b50f4a..96c06dbd 100644 --- a/packages/hoppscotch-sh-admin/src/modules/admin.ts +++ b/packages/hoppscotch-sh-admin/src/modules/admin.ts @@ -1,15 +1,16 @@ import { auth } from '~/helpers/auth'; +import { UNAUTHORIZED } from '~/helpers/errors'; import { HoppModule } from '.'; -const isAdmin = () => { - const user = auth.getCurrentUser(); - return user ? user.isAdmin : false; +const isSetupRoute = (to: unknown) => to === 'setup'; + +const isGuestRoute = (to: unknown) => ['index', 'enter'].includes(to as string); + +const getFirstTimeInfraSetupStatus = async () => { + const isInfraNotSetup = await auth.getFirstTimeInfraSetupStatus(); + return isInfraNotSetup; }; -const GUEST_ROUTES = ['index', 'enter']; - -const isGuestRoute = (to: unknown) => GUEST_ROUTES.includes(to as string); - /** * @module routers */ @@ -24,13 +25,53 @@ const isGuestRoute = (to: unknown) => GUEST_ROUTES.includes(to as string); */ export default { - onBeforeRouteChange(to, from, next) { - if (!isGuestRoute(to.name) && !isAdmin()) { - next({ name: 'index' }); - } else if (isGuestRoute(to.name) && isAdmin()) { - next({ name: 'dashboard' }); - } else { - next(); + async onBeforeRouteChange(to, _from, next) { + const res = await auth.getUserDetails(); + + // Allow performing the silent refresh flow for an invalid access token state + if (res.errors?.[0].message === UNAUTHORIZED) { + return next(); } + + const isAdmin = res.data?.me.isAdmin; + + // Route Guards + if (!isGuestRoute(to.name) && !isAdmin) { + /** + * Reroutes the user to the login page if user is not logged in + * and is not an admin + */ + return next({ name: 'index' }); + } + + if (isAdmin) { + // These route guards applies to the case where the user is logged in successfully and validated as an admin + const isInfraNotSetup = await getFirstTimeInfraSetupStatus(); + + /** + * Reroutes the user to the dashboard homepage if they have setup the infra already + * Else, the Setup page + */ + if (isGuestRoute(to.name)) { + const name = isInfraNotSetup ? 'setup' : 'dashboard'; + return next({ name }); + } + /** + * Reroutes the user to the dashboard homepage if they have setup the infra already + * and are trying to access the setup page + */ + if (isSetupRoute(to.name) && !isInfraNotSetup) { + return next({ name: 'dashboard' }); + } + /** + * Reroutes the user to the setup page if they have not setup the infra yet + * and tries to access a valid route which is not a guest route + */ + if (isInfraNotSetup && !isSetupRoute(to.name)) { + return next({ name: 'setup' }); + } + } + + next(); }, }; diff --git a/packages/hoppscotch-sh-admin/src/pages/setup.vue b/packages/hoppscotch-sh-admin/src/pages/setup.vue new file mode 100644 index 00000000..64af3439 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/pages/setup.vue @@ -0,0 +1,40 @@ + + + + + +meta: + layout: empty + diff --git a/packages/hoppscotch-sh-admin/src/pages/teams/_id.vue b/packages/hoppscotch-sh-admin/src/pages/teams/_id.vue index 17d5b335..285d572a 100644 --- a/packages/hoppscotch-sh-admin/src/pages/teams/_id.vue +++ b/packages/hoppscotch-sh-admin/src/pages/teams/_id.vue @@ -4,7 +4,9 @@ -
{{ t('teams.load_info_error') }}
+
+ {{ t('teams.load_info_error') }} +
diff --git a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue index 49d52576..11090138 100644 --- a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue +++ b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue @@ -1,6 +1,6 @@