feat(sh-admin): introducing data analytics and newsletter configurations (#3845)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com> Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
committed by
GitHub
parent
4798d7bbbd
commit
919579b1da
83
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
83
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
@@ -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']
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div>
|
||||
<SettingsAuthProvider v-model:config="workingConfigs" />
|
||||
<SettingsSmtpConfiguration v-model:config="workingConfigs" />
|
||||
<SettingsDataSharing v-model:config="workingConfigs" />
|
||||
<SettingsReset />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="md:grid md:grid-cols-3 md:gap-4 border-divider border-b py-8">
|
||||
<div class="px-8 md:col-span-1">
|
||||
<h3 class="heading">{{ t('configs.data_sharing.title') }}</h3>
|
||||
<p class="my-1 text-secondaryLight">
|
||||
{{ t('configs.data_sharing.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-8 md:col-span-2">
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t('configs.data_sharing.title') }}
|
||||
</h4>
|
||||
|
||||
<div class="flex items-center space-y-4 py-4">
|
||||
<HoppSmartToggle
|
||||
:on="dataSharingConfigs.enabled"
|
||||
@change="dataSharingConfigs.enabled = !dataSharingConfigs.enabled"
|
||||
>
|
||||
{{ t('configs.data_sharing.toggle_description') }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
|
||||
<!-- TODO: Update the link below -->
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
filled
|
||||
:icon="IconShieldQuestion"
|
||||
:label="t('configs.data_sharing.see_shared')"
|
||||
to="http://docs.hoppscotch.io"
|
||||
blank
|
||||
class="w-min my-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { Config } from '~/composables/useConfigHandler';
|
||||
import IconShieldQuestion from '~icons/lucide/shield-question';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
config: Config;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:config', v: Config): void;
|
||||
}>();
|
||||
|
||||
const workingConfigs = useVModel(props, 'config', emit);
|
||||
|
||||
// Data Sharing Configs
|
||||
const dataSharingConfigs = computed({
|
||||
get() {
|
||||
return workingConfigs.value?.dataSharingConfigs;
|
||||
},
|
||||
set(value) {
|
||||
workingConfigs.value.dataSharingConfigs = value;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center min-h-screen space-y-10"
|
||||
>
|
||||
<div class="flex items-center justify-center flex-col space-y-2">
|
||||
<h2 class="text-lg">{{ t('data_sharing.welcome') }}</h2>
|
||||
<img
|
||||
src="/assets/images/hoppscotch-title.svg"
|
||||
alt="hoppscotch-title"
|
||||
class="w-52"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="bg-primaryLight p-10 border-2 border-dividerLight rounded-lg flex flex-col space-y-8"
|
||||
>
|
||||
<div class="flex flex-col space-y-5 items-start">
|
||||
<div>
|
||||
<p class="text-lg font-bold text-white">
|
||||
{{ t('data_sharing.title') }}
|
||||
</p>
|
||||
<p class="font-light">
|
||||
{{ t('data_sharing.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<HoppSmartToggle
|
||||
:on="dataSharingToggle"
|
||||
@change="dataSharingToggle = !dataSharingToggle"
|
||||
>
|
||||
{{ t('data_sharing.toggle_description') }}
|
||||
</HoppSmartToggle>
|
||||
<!-- TODO: Update link -->
|
||||
<HoppSmartAnchor
|
||||
blank
|
||||
to="http://docs.hoppscotch.io"
|
||||
:label="t('data_sharing.see_shared')"
|
||||
class="underline"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-start space-y-5">
|
||||
<div>
|
||||
<p class="text-lg font-bold text-white">
|
||||
{{ t('newsletter.title') }}
|
||||
</p>
|
||||
<p class="font-light">{{ t('newsletter.description') }}</p>
|
||||
</div>
|
||||
<HoppSmartToggle
|
||||
:on="newsletterToggle"
|
||||
@change="newsletterToggle = !newsletterToggle"
|
||||
>
|
||||
{{ t('newsletter.toggle_description') }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-5">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconLogIn"
|
||||
:label="t('app.continue_to_dashboard')"
|
||||
class="mx-10"
|
||||
@click="submitSelection"
|
||||
/>
|
||||
<HoppSmartAnchor
|
||||
blank
|
||||
to="http://docs.hoppscotch.io"
|
||||
:icon="IconBookOpenText"
|
||||
:label="t('app.read_documentation')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMutation } from '@urql/vue';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { auth } from '~/helpers/auth';
|
||||
import { listmonkApi } from '~/helpers/axiosConfig';
|
||||
import {
|
||||
ServiceStatus,
|
||||
ToggleAnalyticsCollectionDocument,
|
||||
} from '~/helpers/backend/graphql';
|
||||
import IconBookOpenText from '~icons/lucide/book-open-text';
|
||||
import IconLogIn from '~icons/lucide/log-in';
|
||||
|
||||
const t = useI18n();
|
||||
const toast = useToast();
|
||||
const user = auth.getCurrentUser();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'setupComplete', status: boolean): void;
|
||||
}>();
|
||||
|
||||
const dataSharingToggle = ref(true);
|
||||
const newsletterToggle = ref(true);
|
||||
|
||||
// Toggle data sharing
|
||||
const dataSharingMutation = useMutation(ToggleAnalyticsCollectionDocument);
|
||||
|
||||
const toggleDataSharing = async () => {
|
||||
const status = dataSharingToggle.value
|
||||
? ServiceStatus.Enable
|
||||
: ServiceStatus.Disable;
|
||||
|
||||
const result = await dataSharingMutation.executeMutation({ status });
|
||||
|
||||
if (result.error) {
|
||||
toast.error(t('state.data_sharing_failure'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Toggle subscription to newsletter
|
||||
const toggleNewsletter = async () => {
|
||||
try {
|
||||
await listmonkApi.post('/subscription', {
|
||||
email: user?.email,
|
||||
name: user?.displayName,
|
||||
list_uuids: ['f5f0b457-44d0-4aa1-b6f9-165dc1efa56a'],
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error(t('state.newsletter_failure'));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Submit selections made
|
||||
const submitSelection = async () => {
|
||||
const dataSharingResult =
|
||||
dataSharingToggle.value && (await toggleDataSharing());
|
||||
const newsletterResult = newsletterToggle.value && (await toggleNewsletter());
|
||||
|
||||
const setupDataComplete = !dataSharingToggle.value || dataSharingResult;
|
||||
const setupNewsletterComplete = !newsletterToggle.value || newsletterResult;
|
||||
|
||||
emit('setupComplete', setupDataComplete && setupNewsletterComplete);
|
||||
};
|
||||
</script>
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<string, string>;
|
||||
};
|
||||
|
||||
return providerFieldsEmpty || mailFieldsEmpty;
|
||||
const AreAnyConfigFieldsEmpty = (config: Config): boolean => {
|
||||
const sections: Array<ConfigSection> = [
|
||||
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<EnableAndDisableSsoMutation>
|
||||
) => {
|
||||
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 <T, V>(
|
||||
mutation: UseMutationResponse<T>,
|
||||
variables: AnyVariables = undefined,
|
||||
errorMessage: string
|
||||
): Promise<boolean> => {
|
||||
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<EnableAndDisableSsoMutation>
|
||||
) =>
|
||||
executeMutation(
|
||||
updateProviderStatus,
|
||||
{
|
||||
providerInfo:
|
||||
updatedAllowedAuthProviders.value as EnableAndDisableSsoArgs[],
|
||||
},
|
||||
'configs.auth_providers.update_failure'
|
||||
);
|
||||
|
||||
// Updating the infra configurations
|
||||
const updateInfraConfigs = async (
|
||||
const updateInfraConfigs = (
|
||||
updateInfraConfigsMutation: UseMutationResponse<UpdateInfraConfigsMutation>
|
||||
) => {
|
||||
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<ResetInfraConfigsMutation>
|
||||
) => {
|
||||
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<ToggleAnalyticsCollectionMutation>
|
||||
) =>
|
||||
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,
|
||||
|
||||
@@ -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<ListItem[]>([]);
|
||||
const list: Ref<ListItem[]> = 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 () => {
|
||||
|
||||
@@ -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<boolean> => {
|
||||
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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation ToggleAnalyticsCollection($status: ServiceStatus!) {
|
||||
toggleAnalyticsCollection(status: $status)
|
||||
}
|
||||
@@ -29,5 +29,7 @@ export default {
|
||||
token,
|
||||
deviceIdentifier,
|
||||
}),
|
||||
getFirstTimeInfraSetupStatus: () => restApi.get('/site/setup'),
|
||||
updateFirstTimeInfraSetupStatus: () => restApi.put('/site/setup'),
|
||||
logout: () => restApi.get('/auth/logout'),
|
||||
};
|
||||
|
||||
@@ -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 <HoppModule>{
|
||||
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();
|
||||
},
|
||||
};
|
||||
|
||||
40
packages/hoppscotch-sh-admin/src/pages/setup.vue
Normal file
40
packages/hoppscotch-sh-admin/src/pages/setup.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<SetupDataSharingAndNewsletter
|
||||
@setup-complete="(status: boolean) => (isDataSharingAndNewsletterSetup = status)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { auth } from '~/helpers/auth';
|
||||
|
||||
const t = useI18n();
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const isDataSharingAndNewsletterSetup = ref(false);
|
||||
|
||||
// Watcher is added for future proofing as we can have multiple setup steps in future
|
||||
watch(
|
||||
() => isDataSharingAndNewsletterSetup.value,
|
||||
async (status) => {
|
||||
if (status) {
|
||||
const result = await auth.updateFirstTimeInfraSetupStatus();
|
||||
if (result) {
|
||||
toast.success(t('state.setup_success'));
|
||||
router.push('/dashboard');
|
||||
} else {
|
||||
toast.error(t('state.setup_failure'));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: empty
|
||||
</route>
|
||||
@@ -4,7 +4,9 @@
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
|
||||
<div v-else-if="error">{{ t('teams.load_info_error') }}</div>
|
||||
<div v-else-if="error" class="text-lg">
|
||||
{{ t('teams.load_info_error') }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="team" class="flex flex-col">
|
||||
<div class="flex items-center space-x-4">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-if="fetching" class="flex justify-center"><HoppSmartSpinner /></div>
|
||||
<div v-else-if="error">{{ t('users.load_info_error') }}</div>
|
||||
<div v-else-if="error" class="text-lg">{{ t('users.load_info_error') }}</div>
|
||||
<div v-else-if="user" class="flex flex-col space-y-4">
|
||||
<div class="flex gap-x-3">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user