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,15 @@
|
||||
}}</span>
|
||||
{{ t('state.login_as_admin') }}
|
||||
</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
|
||||
class="p-6 bg-primaryLight rounded-lg border border-primaryDark shadow"
|
||||
@@ -143,50 +152,39 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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 IconGoogle from '~icons/auth/google';
|
||||
import IconEmail from '~icons/auth/email';
|
||||
import IconMicrosoft from '~icons/auth/microsoft';
|
||||
import IconArrowLeft from '~icons/lucide/arrow-left';
|
||||
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 toast = useToast();
|
||||
|
||||
const tosLink = import.meta.env.VITE_APP_TOS_LINK;
|
||||
const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK;
|
||||
const allowedAuthProviders = import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS;
|
||||
|
||||
// DATA
|
||||
|
||||
const form = ref({
|
||||
email: '',
|
||||
});
|
||||
const fetching = ref(false);
|
||||
const error = ref(false);
|
||||
const signingInWithGoogle = ref(false);
|
||||
const signingInWithGitHub = ref(false);
|
||||
const signingInWithMicrosoft = ref(false);
|
||||
const signingInWithEmail = ref(false);
|
||||
const mode = ref('sign-in');
|
||||
|
||||
const nonAdminUser = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
const currentUser$ = auth.getCurrentUserStream();
|
||||
const allowedAuthProviders = ref<string[]>([]);
|
||||
|
||||
subscribeToStream(currentUser$, (user) => {
|
||||
if (user && !user.isAdmin) {
|
||||
nonAdminUser.value = true;
|
||||
toast.error(t('state.non_admin_login'));
|
||||
}
|
||||
});
|
||||
onMounted(async () => {
|
||||
allowedAuthProviders.value = await getAllowedAuthProviders();
|
||||
});
|
||||
|
||||
const signInWithGoogle = () => {
|
||||
@@ -241,6 +239,19 @@ const signInWithEmail = async () => {
|
||||
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 () => {
|
||||
try {
|
||||
await auth.signOutUser();
|
||||
|
||||
@@ -65,6 +65,7 @@ import { useSidebar } from '~/composables/useSidebar';
|
||||
import IconDashboard from '~icons/lucide/layout-dashboard';
|
||||
import IconUser from '~icons/lucide/user';
|
||||
import IconUsers from '~icons/lucide/users';
|
||||
import IconSettings from '~icons/lucide/settings';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
|
||||
const t = useI18n();
|
||||
@@ -90,6 +91,12 @@ const primaryNavigations = [
|
||||
to: '/teams',
|
||||
exact: false,
|
||||
},
|
||||
{
|
||||
label: t('settings.settings'),
|
||||
icon: IconSettings,
|
||||
to: '/settings',
|
||||
exact: true,
|
||||
},
|
||||
];
|
||||
</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>
|
||||
Reference in New Issue
Block a user