feat: introducing server configurations in admin dashboard (#3628)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
committed by
GitHub
parent
a8cc569786
commit
f3edd001d7
@@ -6,6 +6,39 @@
|
|||||||
"no_name": "No name",
|
"no_name": "No name",
|
||||||
"open_navigation": "Open Navigation"
|
"open_navigation": "Open Navigation"
|
||||||
},
|
},
|
||||||
|
"configs": {
|
||||||
|
"auth_providers": {
|
||||||
|
"client_id": "CLIENT ID",
|
||||||
|
"client_secret": "CLIENT SECRET",
|
||||||
|
"description": "Configure authentication providers for your server",
|
||||||
|
"title": "Authentication Providers",
|
||||||
|
"update_failure": "Failed to update authentication provider configurations!!"
|
||||||
|
},
|
||||||
|
"confirm_changes": "Hoppscotch server must restart to reflect the new changes. Confirm changes made to the server configurations?",
|
||||||
|
"load_error": "Unable to load server configurations",
|
||||||
|
"mail_configs": {
|
||||||
|
"description": " Configure the smtp configurations",
|
||||||
|
"enable": "Email based authentication",
|
||||||
|
"smtp_url": "MAILER SMTP URL",
|
||||||
|
"address_from": "MAILER FROM ADDRESS",
|
||||||
|
"title": "SMTP Configurations",
|
||||||
|
"update_failure": "Failed to update smtp configurations!!"
|
||||||
|
},
|
||||||
|
"reset": {
|
||||||
|
"confirm_reset": "Hoppscotch server must restart to reflect the new changes. Confirm the reset of server configurations?",
|
||||||
|
"description": "Default configurations will be loaded as specified in the environment file",
|
||||||
|
"failure": "Failed to reset configurations!!",
|
||||||
|
"title": "Reset Configurations",
|
||||||
|
"info": "Reset server configurations"
|
||||||
|
},
|
||||||
|
"restart": {
|
||||||
|
"description": "{duration} seconds remaining before this page reloads automatically",
|
||||||
|
"initiate": "Initiating server restart...",
|
||||||
|
"title": "Server is restarting"
|
||||||
|
},
|
||||||
|
"save_changes": "Save Changes",
|
||||||
|
"title": "Configurations"
|
||||||
|
},
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"no_metrics": "No metrics found",
|
"no_metrics": "No metrics found",
|
||||||
@@ -19,6 +52,9 @@
|
|||||||
"owner": "OWNER",
|
"owner": "OWNER",
|
||||||
"viewer": "VIEWER"
|
"viewer": "VIEWER"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"settings": "Settings"
|
||||||
|
},
|
||||||
"shared_requests": {
|
"shared_requests": {
|
||||||
"clear_filter": "Clear Filter",
|
"clear_filter": "Clear Filter",
|
||||||
"confirm_request_deletion": "Confirm deletion of the selected shared request?",
|
"confirm_request_deletion": "Confirm deletion of the selected shared request?",
|
||||||
@@ -65,6 +101,7 @@
|
|||||||
"email_success": "Email invitation sent successfully",
|
"email_success": "Email invitation sent successfully",
|
||||||
"enter_team_email": "Please enter email of team owner!!",
|
"enter_team_email": "Please enter email of team owner!!",
|
||||||
"error": "Something went wrong",
|
"error": "Something went wrong",
|
||||||
|
"error_auth_providers": "Unable to load auth providers",
|
||||||
"github_signin_failure": "Failed to login with Github",
|
"github_signin_failure": "Failed to login with Github",
|
||||||
"google_signin_failure": "Failed to login with Google",
|
"google_signin_failure": "Failed to login with Google",
|
||||||
"invalid_email": "Please enter a valid email address",
|
"invalid_email": "Please enter a valid email address",
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"@graphql-codegen/urql-introspection": "2.2.1",
|
"@graphql-codegen/urql-introspection": "2.2.1",
|
||||||
"@import-meta-env/cli": "^0.6.3",
|
"@import-meta-env/cli": "^0.6.3",
|
||||||
"@import-meta-env/unplugin": "^0.4.8",
|
"@import-meta-env/unplugin": "^0.4.8",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@vitejs/plugin-vue": "^3.1.0",
|
"@vitejs/plugin-vue": "^3.1.0",
|
||||||
"@vue/compiler-sfc": "^3.2.6",
|
"@vue/compiler-sfc": "^3.2.6",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
|
|||||||
10
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
10
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
@@ -19,18 +19,25 @@ declare module '@vue/runtime-core' {
|
|||||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||||
|
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
|
||||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
||||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||||
|
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
||||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||||
HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable']
|
HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable']
|
||||||
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
||||||
|
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
|
||||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||||
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
|
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
|
||||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['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']
|
||||||
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
||||||
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
||||||
SmartCheckbox: typeof import('./../../hoppscotch-ui/src/components/smart/Checkbox.vue')['default']
|
SmartCheckbox: typeof import('./../../hoppscotch-ui/src/components/smart/Checkbox.vue')['default']
|
||||||
@@ -47,6 +54,7 @@ declare module '@vue/runtime-core' {
|
|||||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
||||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||||
|
SmartSelectWrapper: typeof import('./../../hoppscotch-ui/src/components/smart/SelectWrapper.vue')['default']
|
||||||
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
|
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
|
||||||
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
|
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
|
||||||
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
}}</span>
|
}}</span>
|
||||||
{{ t('state.login_as_admin') }}
|
{{ t('state.login_as_admin') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="fetching" class="flex justify-center py-6">
|
||||||
|
<HoppSmartSpinner />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error">
|
||||||
|
<p class="text-xl">{{ t('state.error') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else class="flex flex-1 flex-col">
|
<div v-else class="flex flex-1 flex-col">
|
||||||
<div
|
<div
|
||||||
class="p-6 bg-primaryLight rounded-lg border border-primaryDark shadow"
|
class="p-6 bg-primaryLight rounded-lg border border-primaryDark shadow"
|
||||||
@@ -143,50 +152,39 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from '~/composables/i18n';
|
||||||
|
import { useToast } from '~/composables/toast';
|
||||||
|
import { auth } from '~/helpers/auth';
|
||||||
|
import { setLocalConfig } from '~/helpers/localpersistence';
|
||||||
|
import IconEmail from '~icons/auth/email';
|
||||||
import IconGithub from '~icons/auth/github';
|
import IconGithub from '~icons/auth/github';
|
||||||
import IconGoogle from '~icons/auth/google';
|
import IconGoogle from '~icons/auth/google';
|
||||||
import IconEmail from '~icons/auth/email';
|
|
||||||
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 { setLocalConfig } from '~/helpers/localpersistence';
|
|
||||||
import { useStreamSubscriber } from '~/composables/stream';
|
|
||||||
import { useToast } from '~/composables/toast';
|
|
||||||
import { auth } from '~/helpers/auth';
|
|
||||||
import { HoppButtonPrimary, HoppButtonSecondary } from '@hoppscotch/ui';
|
|
||||||
import { useI18n } from '~/composables/i18n';
|
|
||||||
|
|
||||||
const { subscribeToStream } = useStreamSubscriber();
|
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const tosLink = import.meta.env.VITE_APP_TOS_LINK;
|
const tosLink = import.meta.env.VITE_APP_TOS_LINK;
|
||||||
const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK;
|
const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK;
|
||||||
const allowedAuthProviders = import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS;
|
|
||||||
|
|
||||||
// DATA
|
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
email: '',
|
email: '',
|
||||||
});
|
});
|
||||||
|
const fetching = ref(false);
|
||||||
|
const error = ref(false);
|
||||||
const signingInWithGoogle = ref(false);
|
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 mode = ref('sign-in');
|
const mode = ref('sign-in');
|
||||||
|
|
||||||
const nonAdminUser = ref(false);
|
const nonAdminUser = ref(false);
|
||||||
|
|
||||||
onMounted(() => {
|
const allowedAuthProviders = ref<string[]>([]);
|
||||||
const currentUser$ = auth.getCurrentUserStream();
|
|
||||||
|
|
||||||
subscribeToStream(currentUser$, (user) => {
|
onMounted(async () => {
|
||||||
if (user && !user.isAdmin) {
|
allowedAuthProviders.value = await getAllowedAuthProviders();
|
||||||
nonAdminUser.value = true;
|
|
||||||
toast.error(t('state.non_admin_login'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const signInWithGoogle = () => {
|
const signInWithGoogle = () => {
|
||||||
@@ -241,6 +239,19 @@ const signInWithEmail = async () => {
|
|||||||
signingInWithEmail.value = false;
|
signingInWithEmail.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAllowedAuthProviders = async () => {
|
||||||
|
fetching.value = true;
|
||||||
|
try {
|
||||||
|
const res = await auth.getAllowedAuthProviders();
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
error.value = true;
|
||||||
|
toast.error(t('state.error_auth_providers'));
|
||||||
|
} finally {
|
||||||
|
fetching.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
try {
|
try {
|
||||||
await auth.signOutUser();
|
await auth.signOutUser();
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import { useSidebar } from '~/composables/useSidebar';
|
|||||||
import IconDashboard from '~icons/lucide/layout-dashboard';
|
import IconDashboard from '~icons/lucide/layout-dashboard';
|
||||||
import IconUser from '~icons/lucide/user';
|
import IconUser from '~icons/lucide/user';
|
||||||
import IconUsers from '~icons/lucide/users';
|
import IconUsers from '~icons/lucide/users';
|
||||||
|
import IconSettings from '~icons/lucide/settings';
|
||||||
import { useI18n } from '~/composables/i18n';
|
import { useI18n } from '~/composables/i18n';
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -90,6 +91,12 @@ const primaryNavigations = [
|
|||||||
to: '/teams',
|
to: '/teams',
|
||||||
exact: false,
|
exact: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t('settings.settings'),
|
||||||
|
icon: IconSettings,
|
||||||
|
to: '/settings',
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="workingConfigs"
|
||||||
|
class="md:grid md:grid-cols-3 md:gap-4 border-divider border-b"
|
||||||
|
>
|
||||||
|
<div class="pb-8 px-8 md:col-span-1">
|
||||||
|
<h3 class="heading">{{ t('configs.auth_providers.title') }}</h3>
|
||||||
|
<p class="my-1 text-secondaryLight">
|
||||||
|
{{ t('configs.auth_providers.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-8 p-8 md:col-span-2">
|
||||||
|
<section>
|
||||||
|
<h4 class="font-semibold text-secondaryDark">
|
||||||
|
{{ t('configs.auth_providers.title') }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="provider in workingConfigs.providers"
|
||||||
|
class="space-y-4 py-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<HoppSmartToggle
|
||||||
|
:on="provider.enabled"
|
||||||
|
@change="provider.enabled = !provider.enabled"
|
||||||
|
>
|
||||||
|
{{ capitalize(provider.name) }}
|
||||||
|
</HoppSmartToggle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="provider.enabled" class="ml-12">
|
||||||
|
<div
|
||||||
|
v-for="field in providerConfigFields"
|
||||||
|
:key="field.key"
|
||||||
|
class="mt-5"
|
||||||
|
>
|
||||||
|
<label>{{ field.name }}</label>
|
||||||
|
<span class="flex">
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="provider.fields[field.key]"
|
||||||
|
:type="
|
||||||
|
isMasked(provider.name, field.key) ? 'password' : 'text'
|
||||||
|
"
|
||||||
|
:disabled="isMasked(provider.name, field.key)"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:icon="
|
||||||
|
isMasked(provider.name, field.key) ? IconEye : IconEyeOff
|
||||||
|
"
|
||||||
|
class="bg-primaryLight h-9 mt-2"
|
||||||
|
@click="toggleMask(provider.name, field.key)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { reactive } from 'vue';
|
||||||
|
import { useI18n } from '~/composables/i18n';
|
||||||
|
import { Config, SsoAuthProviders } from '~/composables/useConfigHandler';
|
||||||
|
import IconEye from '~icons/lucide/eye';
|
||||||
|
import IconEyeOff from '~icons/lucide/eye-off';
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: Config;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:config', v: Config): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const workingConfigs = useVModel(props, 'config', emit);
|
||||||
|
|
||||||
|
// Capitalize first letter of a string
|
||||||
|
const capitalize = (text: string) =>
|
||||||
|
text.charAt(0).toUpperCase() + text.slice(1);
|
||||||
|
|
||||||
|
// Masking sensitive fields
|
||||||
|
type Field = {
|
||||||
|
name: string;
|
||||||
|
key: keyof Config['providers']['google' | 'github' | 'microsoft']['fields'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const providerConfigFields = reactive<Field[]>([
|
||||||
|
{ name: t('configs.auth_providers.client_id'), key: 'client_id' },
|
||||||
|
{ name: t('configs.auth_providers.client_secret'), key: 'client_secret' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const maskState = reactive({
|
||||||
|
google: {
|
||||||
|
client_id: true,
|
||||||
|
client_secret: true,
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
client_id: true,
|
||||||
|
client_secret: true,
|
||||||
|
},
|
||||||
|
microsoft: {
|
||||||
|
client_id: true,
|
||||||
|
client_secret: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleMask = (
|
||||||
|
provider: SsoAuthProviders,
|
||||||
|
fieldKey: keyof Config['providers'][
|
||||||
|
| 'google'
|
||||||
|
| 'github'
|
||||||
|
| 'microsoft']['fields']
|
||||||
|
) => {
|
||||||
|
maskState[provider][fieldKey] = !maskState[provider][fieldKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMasked = (
|
||||||
|
provider: SsoAuthProviders,
|
||||||
|
fieldKey: keyof Config['providers'][
|
||||||
|
| 'google'
|
||||||
|
| 'github'
|
||||||
|
| 'microsoft']['fields']
|
||||||
|
) => maskState[provider][fieldKey];
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<SettingsAuthProvider v-model:config="workingConfigs" />
|
||||||
|
<SettingsSmtpConfiguration v-model:config="workingConfigs" />
|
||||||
|
<SettingsReset />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { Config } from '~/composables/useConfigHandler';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: Config;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:config', v: Config): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const workingConfigs = useVModel(props, 'config', emit);
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div class="md:grid md:grid-cols-3 md:gap-4">
|
||||||
|
<div class="p-8 md:col-span-1">
|
||||||
|
<h3 class="heading">{{ t('configs.reset.title') }}</h3>
|
||||||
|
<p class="my-1 text-secondaryLight">
|
||||||
|
{{ t('configs.reset.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-8 p-8 md:col-span-2">
|
||||||
|
<section>
|
||||||
|
<h4 class="font-semibold text-secondaryDark">
|
||||||
|
{{ t('configs.reset.info') }}
|
||||||
|
</h4>
|
||||||
|
<div class="space-y-4 py-4">
|
||||||
|
<div>
|
||||||
|
<HoppButtonPrimary
|
||||||
|
:label="t('configs.reset.title')"
|
||||||
|
class="bg-red-700 hover:bg-red-500"
|
||||||
|
@click="resetModal = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingsServerRestart v-if="resetInfraConfigs" :reset="resetInfraConfigs" />
|
||||||
|
|
||||||
|
<HoppSmartConfirmModal
|
||||||
|
:show="resetModal"
|
||||||
|
:title="t('configs.reset.confirm_reset')"
|
||||||
|
@hide-modal="resetModal = false"
|
||||||
|
@resolve="resetInfraConfigs = true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useI18n } from '~/composables/i18n';
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
const resetModal = ref(false);
|
||||||
|
const resetInfraConfigs = ref(false);
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<HoppSmartModal
|
||||||
|
v-if="restart"
|
||||||
|
:dimissible="false"
|
||||||
|
:title="t('configs.restart.title')"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="text-center">
|
||||||
|
{{ t('configs.restart.description', { duration }) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</HoppSmartModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useMutation } from '@urql/vue';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from '~/composables/i18n';
|
||||||
|
import { useToast } from '~/composables/toast';
|
||||||
|
import { Config, useConfigHandler } from '~/composables/useConfigHandler';
|
||||||
|
import {
|
||||||
|
EnableAndDisableSsoDocument,
|
||||||
|
ResetInfraConfigsDocument,
|
||||||
|
UpdateInfraConfigsDocument,
|
||||||
|
} from '~/helpers/backend/graphql';
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
workingConfigs?: Config;
|
||||||
|
reset?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
reset: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mutations to update or reset server configurations and audit logs
|
||||||
|
const resetInfraConfigsMutation = useMutation(ResetInfraConfigsDocument);
|
||||||
|
const updateInfraConfigsMutation = useMutation(UpdateInfraConfigsDocument);
|
||||||
|
const updateAllowedAuthProviderMutation = useMutation(
|
||||||
|
EnableAndDisableSsoDocument
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mutation handlers
|
||||||
|
const { updateInfraConfigs, updateAuthProvider, resetInfraConfigs } =
|
||||||
|
useConfigHandler(props.workingConfigs);
|
||||||
|
|
||||||
|
// Call relevant mutations on component mount and initiate server restart
|
||||||
|
const duration = ref(30);
|
||||||
|
const restart = ref(false);
|
||||||
|
|
||||||
|
// Start countdown timer
|
||||||
|
const startCountdown = () => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
duration.value--;
|
||||||
|
if (duration.value === 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
toast.success(t('configs.restart.initiate'));
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call relevant mutations on component mount and initiate server restart
|
||||||
|
onMounted(async () => {
|
||||||
|
let success = true;
|
||||||
|
|
||||||
|
if (props.reset) {
|
||||||
|
success = await resetInfraConfigs(resetInfraConfigsMutation);
|
||||||
|
if (!success) return;
|
||||||
|
} else {
|
||||||
|
const authResult = await updateAuthProvider(
|
||||||
|
updateAllowedAuthProviderMutation
|
||||||
|
);
|
||||||
|
const infraResult = await updateInfraConfigs(updateInfraConfigsMutation);
|
||||||
|
|
||||||
|
success = authResult && infraResult;
|
||||||
|
if (!success) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
restart.value = true;
|
||||||
|
startCountdown();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="smtpConfigs"
|
||||||
|
class="md:grid md:grid-cols-3 md:gap-4 border-divider border-b"
|
||||||
|
>
|
||||||
|
<div class="p-8 px-8 md:col-span-1">
|
||||||
|
<h3 class="heading">{{ t('configs.mail_configs.title') }}</h3>
|
||||||
|
<p class="my-1 text-secondaryLight">
|
||||||
|
{{ t('configs.mail_configs.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-8 p-8 md:col-span-2">
|
||||||
|
<section>
|
||||||
|
<h4 class="font-semibold text-secondaryDark">
|
||||||
|
{{ t('configs.mail_configs.title') }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="space-y-4 py-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<HoppSmartToggle
|
||||||
|
:on="smtpConfigs.enabled"
|
||||||
|
@change="smtpConfigs.enabled = !smtpConfigs.enabled"
|
||||||
|
>
|
||||||
|
{{ t('configs.mail_configs.enable') }}
|
||||||
|
</HoppSmartToggle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="smtpConfigs.enabled" class="ml-12">
|
||||||
|
<div
|
||||||
|
v-for="field in smtpConfigFields"
|
||||||
|
:key="field.key"
|
||||||
|
class="mt-5"
|
||||||
|
>
|
||||||
|
<label>{{ field.name }}</label>
|
||||||
|
<span class="flex">
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="smtpConfigs.fields[field.key]"
|
||||||
|
:type="isMasked(field.key) ? 'password' : 'text'"
|
||||||
|
:disabled="isMasked(field.key)"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:icon="isMasked(field.key) ? IconEye : IconEyeOff"
|
||||||
|
class="bg-primaryLight h-9 mt-2"
|
||||||
|
@click="toggleMask(field.key)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { computed, reactive } from 'vue';
|
||||||
|
import { useI18n } from '~/composables/i18n';
|
||||||
|
import { Config } from '~/composables/useConfigHandler';
|
||||||
|
import IconEye from '~icons/lucide/eye';
|
||||||
|
import IconEyeOff from '~icons/lucide/eye-off';
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: Config;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:config', v: Config): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const workingConfigs = useVModel(props, 'config', emit);
|
||||||
|
|
||||||
|
// Get or set smtpConfigs from workingConfigs
|
||||||
|
const smtpConfigs = computed({
|
||||||
|
get() {
|
||||||
|
return workingConfigs.value?.mailConfigs;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
workingConfigs.value.mailConfigs = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mask sensitive fields
|
||||||
|
type Field = {
|
||||||
|
name: string;
|
||||||
|
key: keyof Config['mailConfigs']['fields'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const smtpConfigFields = reactive<Field[]>([
|
||||||
|
{ name: t('configs.mail_configs.smtp_url'), key: 'mailer_smtp_url' },
|
||||||
|
{ name: t('configs.mail_configs.address_from'), key: 'mailer_from_address' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const maskState = reactive<Record<string, boolean>>({
|
||||||
|
mailer_smtp_url: true,
|
||||||
|
mailer_from_address: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleMask = (fieldKey: keyof Config['mailConfigs']['fields']) => {
|
||||||
|
maskState[fieldKey] = !maskState[fieldKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMasked = (fieldKey: keyof Config['mailConfigs']['fields']) =>
|
||||||
|
maskState[fieldKey];
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { TypedDocumentNode, useClientHandle } from '@urql/vue';
|
||||||
|
import { DocumentNode } from 'graphql';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
/** A composable function to handle grapqhl requests
|
||||||
|
* using urql's useClientHandle
|
||||||
|
* @param query The query to be executed
|
||||||
|
* @param getList A function to get the list from the result
|
||||||
|
* @param variables The variables to be passed to the query
|
||||||
|
*/
|
||||||
|
export function useClientHandler<
|
||||||
|
Result,
|
||||||
|
Vars extends Record<string, any>,
|
||||||
|
ListItem
|
||||||
|
>(
|
||||||
|
query: string | TypedDocumentNode<Result, Vars> | DocumentNode,
|
||||||
|
getList: (result: Result) => ListItem[],
|
||||||
|
variables: Vars
|
||||||
|
) {
|
||||||
|
const { client } = useClientHandle();
|
||||||
|
const fetching = ref(true);
|
||||||
|
const error = ref(false);
|
||||||
|
const list = ref<ListItem[]>([]);
|
||||||
|
|
||||||
|
const fetchList = async () => {
|
||||||
|
fetching.value = true;
|
||||||
|
try {
|
||||||
|
const result = await client
|
||||||
|
.query(query, {
|
||||||
|
...variables,
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
const resultList = getList(result.data!);
|
||||||
|
|
||||||
|
list.value.push(...resultList);
|
||||||
|
} catch (e) {
|
||||||
|
error.value = true;
|
||||||
|
}
|
||||||
|
fetching.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetching,
|
||||||
|
error,
|
||||||
|
list,
|
||||||
|
fetchList,
|
||||||
|
};
|
||||||
|
}
|
||||||
345
packages/hoppscotch-sh-admin/src/composables/useConfigHandler.ts
Normal file
345
packages/hoppscotch-sh-admin/src/composables/useConfigHandler.ts
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
import { UseMutationResponse } from '@urql/vue';
|
||||||
|
import { useClientHandler } from './useClientHandler';
|
||||||
|
import { useToast } from './toast';
|
||||||
|
import { useI18n } from '~/composables/i18n';
|
||||||
|
import {
|
||||||
|
InfraConfigEnum,
|
||||||
|
InfraConfigsDocument,
|
||||||
|
AllowedAuthProvidersDocument,
|
||||||
|
EnableAndDisableSsoMutation,
|
||||||
|
UpdateInfraConfigsMutation,
|
||||||
|
ResetInfraConfigsMutation,
|
||||||
|
EnableAndDisableSsoArgs,
|
||||||
|
InfraConfigArgs,
|
||||||
|
} from '~/helpers/backend/graphql';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export type SsoAuthProviders = 'google' | 'microsoft' | 'github';
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
providers: {
|
||||||
|
google: {
|
||||||
|
name: SsoAuthProviders;
|
||||||
|
enabled: boolean;
|
||||||
|
fields: {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
github: {
|
||||||
|
name: SsoAuthProviders;
|
||||||
|
enabled: boolean;
|
||||||
|
fields: {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
microsoft: {
|
||||||
|
name: SsoAuthProviders;
|
||||||
|
enabled: boolean;
|
||||||
|
fields: {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mailConfigs: {
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
fields: {
|
||||||
|
mailer_smtp_url: string;
|
||||||
|
mailer_from_address: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdatedConfigs = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Composable that handles all operations related to server configurations
|
||||||
|
* @param updatedConfigs A Config Object contatining the updated configs
|
||||||
|
*/
|
||||||
|
export function useConfigHandler(updatedConfigs?: Config) {
|
||||||
|
const t = useI18n();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// Fetching infra configurations
|
||||||
|
const {
|
||||||
|
fetching: fetchingInfraConfigs,
|
||||||
|
error: infraConfigsError,
|
||||||
|
list: infraConfigs,
|
||||||
|
fetchList: fetchInfraConfigs,
|
||||||
|
} = useClientHandler(InfraConfigsDocument, (x) => x.infraConfigs, {
|
||||||
|
configNames: [
|
||||||
|
'GOOGLE_CLIENT_ID',
|
||||||
|
'GOOGLE_CLIENT_SECRET',
|
||||||
|
'MICROSOFT_CLIENT_ID',
|
||||||
|
'MICROSOFT_CLIENT_SECRET',
|
||||||
|
'GITHUB_CLIENT_ID',
|
||||||
|
'GITHUB_CLIENT_SECRET',
|
||||||
|
'MAILER_SMTP_URL',
|
||||||
|
'MAILER_ADDRESS_FROM',
|
||||||
|
] as InfraConfigEnum[],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetching allowed auth providers
|
||||||
|
const {
|
||||||
|
fetching: fetchingAllowedAuthProviders,
|
||||||
|
error: allowedAuthProvidersError,
|
||||||
|
list: allowedAuthProviders,
|
||||||
|
fetchList: fetchAllowedAuthProviders,
|
||||||
|
} = useClientHandler(
|
||||||
|
AllowedAuthProvidersDocument,
|
||||||
|
(x) => x.allowedAuthProviders,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Current and working configs
|
||||||
|
const currentConfigs = ref<Config>();
|
||||||
|
const workingConfigs = ref<Config>();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchInfraConfigs();
|
||||||
|
await fetchAllowedAuthProviders();
|
||||||
|
|
||||||
|
// Transforming the fetched data into a Configs object
|
||||||
|
currentConfigs.value = {
|
||||||
|
providers: {
|
||||||
|
google: {
|
||||||
|
name: 'google',
|
||||||
|
enabled: allowedAuthProviders.value.includes('GOOGLE'),
|
||||||
|
fields: {
|
||||||
|
client_id:
|
||||||
|
infraConfigs.value.find((x) => x.name === 'GOOGLE_CLIENT_ID')
|
||||||
|
?.value ?? '',
|
||||||
|
client_secret:
|
||||||
|
infraConfigs.value.find((x) => x.name === 'GOOGLE_CLIENT_SECRET')
|
||||||
|
?.value ?? '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
name: 'github',
|
||||||
|
enabled: allowedAuthProviders.value.includes('GITHUB'),
|
||||||
|
fields: {
|
||||||
|
client_id:
|
||||||
|
infraConfigs.value.find((x) => x.name === 'GITHUB_CLIENT_ID')
|
||||||
|
?.value ?? '',
|
||||||
|
client_secret:
|
||||||
|
infraConfigs.value.find((x) => x.name === 'GITHUB_CLIENT_SECRET')
|
||||||
|
?.value ?? '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
microsoft: {
|
||||||
|
name: 'microsoft',
|
||||||
|
enabled: allowedAuthProviders.value.includes('MICROSOFT'),
|
||||||
|
fields: {
|
||||||
|
client_id:
|
||||||
|
infraConfigs.value.find((x) => x.name === 'MICROSOFT_CLIENT_ID')
|
||||||
|
?.value ?? '',
|
||||||
|
client_secret:
|
||||||
|
infraConfigs.value.find(
|
||||||
|
(x) => x.name === 'MICROSOFT_CLIENT_SECRET'
|
||||||
|
)?.value ?? '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mailConfigs: {
|
||||||
|
name: 'email',
|
||||||
|
enabled: allowedAuthProviders.value.includes('EMAIL'),
|
||||||
|
fields: {
|
||||||
|
mailer_smtp_url:
|
||||||
|
infraConfigs.value.find((x) => x.name === 'MAILER_SMTP_URL')
|
||||||
|
?.value ?? '',
|
||||||
|
mailer_from_address:
|
||||||
|
infraConfigs.value.find((x) => x.name === 'MAILER_ADDRESS_FROM')
|
||||||
|
?.value ?? '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cloning the current configs to working configs
|
||||||
|
// Changes are made only to working configs
|
||||||
|
workingConfigs.value = cloneDeep(currentConfigs.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trasforming the working configs back into the format required by the mutations
|
||||||
|
const updatedInfraConfigs = computed(() => {
|
||||||
|
let config: UpdatedConfigs[] = [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (updatedConfigs?.providers.google.enabled) {
|
||||||
|
config.push(
|
||||||
|
{
|
||||||
|
name: 'GOOGLE_CLIENT_ID',
|
||||||
|
value: updatedConfigs?.providers.google.fields.client_id ?? '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GOOGLE_CLIENT_SECRET',
|
||||||
|
value: updatedConfigs?.providers.google.fields.client_secret ?? '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
config = config.filter(
|
||||||
|
(item) =>
|
||||||
|
item.name !== 'GOOGLE_CLIENT_ID' &&
|
||||||
|
item.name !== 'GOOGLE_CLIENT_SECRET'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (updatedConfigs?.providers.microsoft.enabled) {
|
||||||
|
config.push(
|
||||||
|
{
|
||||||
|
name: 'MICROSOFT_CLIENT_ID',
|
||||||
|
value: updatedConfigs?.providers.microsoft.fields.client_id ?? '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MICROSOFT_CLIENT_SECRET',
|
||||||
|
value: updatedConfigs?.providers.microsoft.fields.client_secret ?? '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
config = config.filter(
|
||||||
|
(item) =>
|
||||||
|
item.name !== 'MICROSOFT_CLIENT_ID' &&
|
||||||
|
item.name !== 'MICROSOFT_CLIENT_SECRET'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedConfigs?.providers.github.enabled) {
|
||||||
|
config.push(
|
||||||
|
{
|
||||||
|
name: 'GITHUB_CLIENT_ID',
|
||||||
|
value: updatedConfigs?.providers.github.fields.client_id ?? '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GITHUB_CLIENT_SECRET',
|
||||||
|
value: updatedConfigs?.providers.github.fields.client_secret ?? '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
config = config.filter(
|
||||||
|
(item) =>
|
||||||
|
item.name !== 'GITHUB_CLIENT_ID' &&
|
||||||
|
item.name !== 'GITHUB_CLIENT_SECRET'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedConfigs?.mailConfigs.enabled) {
|
||||||
|
config.push(
|
||||||
|
{
|
||||||
|
name: 'MAILER_SMTP_URL',
|
||||||
|
value: updatedConfigs?.mailConfigs.fields.mailer_smtp_url ?? '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MAILER_ADDRESS_FROM',
|
||||||
|
value: updatedConfigs?.mailConfigs.fields.mailer_from_address ?? '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
config = config.filter(
|
||||||
|
(item) =>
|
||||||
|
item.name !== 'MAILER_SMTP_URL' && item.name !== 'MAILER_ADDRESS_FROM'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
config = config.filter((item) => item.name !== '');
|
||||||
|
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trasforming the working configs back into the format required by the mutations
|
||||||
|
const updatedAllowedAuthProviders = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
provider: 'GOOGLE',
|
||||||
|
status: updatedConfigs?.providers.google.enabled ? 'ENABLE' : 'DISABLE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provider: 'MICROSOFT',
|
||||||
|
status: updatedConfigs?.providers.microsoft.enabled
|
||||||
|
? 'ENABLE'
|
||||||
|
: 'DISABLE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provider: 'GITHUB',
|
||||||
|
status: updatedConfigs?.providers.github.enabled ? 'ENABLE' : 'DISABLE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provider: 'EMAIL',
|
||||||
|
status: updatedConfigs?.mailConfigs.enabled ? 'ENABLE' : 'DISABLE',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Updating the auth provider configurations
|
||||||
|
const updateAuthProvider = async (
|
||||||
|
updateProviderStatus: UseMutationResponse<EnableAndDisableSsoMutation>
|
||||||
|
) => {
|
||||||
|
const variables = {
|
||||||
|
providerInfo:
|
||||||
|
updatedAllowedAuthProviders.value as EnableAndDisableSsoArgs[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await updateProviderStatus.executeMutation(variables);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
toast.error(t('configs.auth_providers.update_failure'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Updating the infra configurations
|
||||||
|
const updateInfraConfigs = async (
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resetting the infra configurations
|
||||||
|
const resetInfraConfigs = async (
|
||||||
|
resetInfraConfigsMutation: UseMutationResponse<ResetInfraConfigsMutation>
|
||||||
|
) => {
|
||||||
|
const result = await resetInfraConfigsMutation.executeMutation();
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
toast.error(t('configs.reset.failure'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentConfigs,
|
||||||
|
workingConfigs,
|
||||||
|
updatedInfraConfigs,
|
||||||
|
updatedAllowedAuthProviders,
|
||||||
|
updateAuthProvider,
|
||||||
|
updateInfraConfigs,
|
||||||
|
resetInfraConfigs,
|
||||||
|
fetchingInfraConfigs,
|
||||||
|
fetchingAllowedAuthProviders,
|
||||||
|
infraConfigsError,
|
||||||
|
allowedAuthProvidersError,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -227,4 +227,9 @@ export const auth = {
|
|||||||
window.location.href = import.meta.env.VITE_ADMIN_URL;
|
window.location.href = import.meta.env.VITE_ADMIN_URL;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getAllowedAuthProviders: async () => {
|
||||||
|
const res = await authQuery.getProviders();
|
||||||
|
return res.data?.providers;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mutation EnableAndDisableSSO($providerInfo: [EnableAndDisableSSOArgs!]!) {
|
||||||
|
enableAndDisableSSO(providerInfo: $providerInfo)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mutation ResetInfraConfigs {
|
||||||
|
resetInfraConfigs
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
mutation UpdateInfraConfigs($infraConfigs: [InfraConfigArgs!]!) {
|
||||||
|
updateInfraConfigs(infraConfigs: $infraConfigs) {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
query AllowedAuthProviders {
|
||||||
|
allowedAuthProviders
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
query InfraConfigs($configNames: [InfraConfigEnum!]!) {
|
||||||
|
infraConfigs(configNames: $configNames) {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ export default {
|
|||||||
}),
|
}),
|
||||||
refreshToken: () => restApi.get('/auth/refresh'),
|
refreshToken: () => restApi.get('/auth/refresh'),
|
||||||
elevateUser: () => restApi.get('/auth/verify/admin'),
|
elevateUser: () => restApi.get('/auth/verify/admin'),
|
||||||
|
getProviders: () => restApi.get('/auth/providers'),
|
||||||
sendMagicLink: (email: string) =>
|
sendMagicLink: (email: string) =>
|
||||||
restApi.post('/auth/signin?origin=admin', {
|
restApi.post('/auth/signin?origin=admin', {
|
||||||
email,
|
email,
|
||||||
|
|||||||
@@ -1,3 +1,81 @@
|
|||||||
<template>
|
<template>
|
||||||
<h3 class="sm:px-6 p-4 text-3xl font-medium text-gray-200">Settings</h3>
|
<div class="flex flex-col">
|
||||||
|
<h1 class="text-lg font-bold text-secondaryDark">
|
||||||
|
{{ t('settings.settings') }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="fetchingInfraConfigs || fetchingAllowedAuthProviders"
|
||||||
|
class="flex justify-center"
|
||||||
|
>
|
||||||
|
<HoppSmartSpinner />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="infraConfigsError || allowedAuthProvidersError">
|
||||||
|
{{ t('configs.load_error') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col py-8">
|
||||||
|
<HoppSmartTabs v-model="selectedOptionTab" render-inactive-tabs>
|
||||||
|
<HoppSmartTab :id="'config'" :label="t('configs.title')">
|
||||||
|
<SettingsConfigurations
|
||||||
|
v-model:config="workingConfigs"
|
||||||
|
class="py-8 px-4"
|
||||||
|
/>
|
||||||
|
</HoppSmartTab>
|
||||||
|
</HoppSmartTabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isConfigUpdated" class="fixed bottom-0 right-0 m-10">
|
||||||
|
<HoppButtonPrimary
|
||||||
|
:label="t('configs.save_changes')"
|
||||||
|
@click="showSaveChangesModal = !showSaveChangesModal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingsServerRestart
|
||||||
|
v-if="initiateServerRestart"
|
||||||
|
:workingConfigs="workingConfigs"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HoppSmartConfirmModal
|
||||||
|
:show="showSaveChangesModal"
|
||||||
|
:title="t('configs.confirm_changes')"
|
||||||
|
@hide-modal="showSaveChangesModal = false"
|
||||||
|
@resolve="initiateServerRestart = true"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from '~/composables/i18n';
|
||||||
|
import { useConfigHandler } from '~/composables/useConfigHandler';
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
const showSaveChangesModal = ref(false);
|
||||||
|
const initiateServerRestart = ref(false);
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
type OptionTabs = 'config';
|
||||||
|
const selectedOptionTab = ref<OptionTabs>('config');
|
||||||
|
|
||||||
|
// Obtain the current and working configs from the useConfigHandler composable
|
||||||
|
const {
|
||||||
|
currentConfigs,
|
||||||
|
workingConfigs,
|
||||||
|
fetchingInfraConfigs,
|
||||||
|
infraConfigsError,
|
||||||
|
fetchingAllowedAuthProviders,
|
||||||
|
allowedAuthProvidersError,
|
||||||
|
} = useConfigHandler();
|
||||||
|
|
||||||
|
// Check if the configs have been updated
|
||||||
|
const isConfigUpdated = computed(() =>
|
||||||
|
currentConfigs.value && workingConfigs.value
|
||||||
|
? !isEqual(currentConfigs.value, workingConfigs.value)
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,16 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<HoppSmartModal v-if="show" dialog :title="confirm ?? t?.('modal.confirm') ?? 'Confirm'" role="dialog" aria-modal="true"
|
<HoppSmartModal
|
||||||
@close="hideModal">
|
v-if="show"
|
||||||
|
dialog
|
||||||
|
:title="confirm ?? t?.('modal.confirm') ?? 'Confirm'"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
@close="hideModal"
|
||||||
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col items-center justify-center">
|
<div class="text-center">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
<HoppButtonPrimary v-focus :label="yes ?? t?.('action.yes') ?? 'Yes'" :loading="!!loadingState" outline
|
<HoppButtonPrimary
|
||||||
@click="resolve" />
|
v-focus
|
||||||
<HoppButtonSecondary :label="no ?? t?.('action.no') ?? 'No'" filled outline @click="hideModal" />
|
:label="yes ?? t?.('action.yes') ?? 'Yes'"
|
||||||
|
:loading="!!loadingState"
|
||||||
|
outline
|
||||||
|
@click="resolve"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="no ?? t?.('action.no') ?? 'No'"
|
||||||
|
filled
|
||||||
|
outline
|
||||||
|
@click="hideModal"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartModal>
|
</HoppSmartModal>
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -1350,6 +1350,9 @@ importers:
|
|||||||
'@import-meta-env/unplugin':
|
'@import-meta-env/unplugin':
|
||||||
specifier: ^0.4.8
|
specifier: ^0.4.8
|
||||||
version: 0.4.8(@import-meta-env/cli@0.6.3)(dotenv@16.3.1)
|
version: 0.4.8(@import-meta-env/cli@0.6.3)(dotenv@16.3.1)
|
||||||
|
'@types/lodash-es':
|
||||||
|
specifier: ^4.17.12
|
||||||
|
version: 4.17.12
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.2.0(vite@3.2.4)(vue@3.3.9)
|
version: 3.2.0(vite@3.2.4)(vue@3.3.9)
|
||||||
|
|||||||
Reference in New Issue
Block a user