feat(sh-admin): introduce input validations to server configurations (#4642)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Joel Jacob Stephen 2025-02-19 08:41:20 -06:00 committed by GitHub
parent 4200b00c04
commit 7f84c52c83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 68 additions and 5 deletions

View file

@ -31,6 +31,7 @@
}, },
"confirm_changes": "Hoppscotch server must restart to reflect the new changes. Confirm changes made to the server configurations?", "confirm_changes": "Hoppscotch server must restart to reflect the new changes. Confirm changes made to the server configurations?",
"input_empty": "Please fill all the fields before updating the configurations", "input_empty": "Please fill all the fields before updating the configurations",
"input_validation_error": "Some fields have invalid values. Please correct them before updating the configurations",
"data_sharing": { "data_sharing": {
"title": "Data Sharing", "title": "Data Sharing",
"description": "Help improve Hoppscotch by sharing anonymous data", "description": "Help improve Hoppscotch by sharing anonymous data",
@ -48,6 +49,7 @@
"enable_email_auth": "Enable Email based authentication", "enable_email_auth": "Enable Email based authentication",
"enable_smtp": "Enable SMTP", "enable_smtp": "Enable SMTP",
"host": "MAILER HOST", "host": "MAILER HOST",
"input_validation": "SMTP URL should start with smtp(s)://",
"password": "MAILER PASSWORD", "password": "MAILER PASSWORD",
"port": "MAILER PORT", "port": "MAILER PORT",
"secure": "MAILER SECURE", "secure": "MAILER SECURE",

View file

@ -40,8 +40,10 @@ declare module 'vue' {
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default'] IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default'] IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
IconLucideInbox: typeof import('~icons/lucide/inbox')['default'] IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
IconLucideInfo: typeof import('~icons/lucide/info')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default'] IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUser: typeof import('~icons/lucide/user')['default'] IconLucideUser: typeof import('~icons/lucide/user')['default']
IconLucideX: typeof import('~icons/lucide/x')['default']
SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default'] SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default']
SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default'] SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default']
SettingsDataSharing: typeof import('./components/settings/DataSharing.vue')['default'] SettingsDataSharing: typeof import('./components/settings/DataSharing.vue')['default']

View file

@ -87,6 +87,16 @@
@click="toggleMask(field.key)" @click="toggleMask(field.key)"
/> />
</span> </span>
<div
v-if="getFieldError(field.key)"
class="flex items-center justify-between bg-red-200 px-2 py-2 font-semibold text-red-700 rounded-lg mt-2 max-w-lg"
>
<div class="flex items-center">
<icon-lucide-info class="mr-2" />
<span> {{ field.error }} </span>
</div>
</div>
</span> </span>
</div> </div>
</div> </div>
@ -99,9 +109,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { computed, reactive } from 'vue'; import { computed, reactive, watch } from 'vue';
import { useI18n } from '~/composables/i18n'; import { useI18n } from '~/composables/i18n';
import { ServerConfigs } from '~/helpers/configs'; import { hasInputValidationFailed, ServerConfigs } from '~/helpers/configs';
import IconEye from '~icons/lucide/eye'; import IconEye from '~icons/lucide/eye';
import IconEyeOff from '~icons/lucide/eye-off'; import IconEyeOff from '~icons/lucide/eye-off';
import IconHelpCircle from '~icons/lucide/help-circle'; import IconHelpCircle from '~icons/lucide/help-circle';
@ -132,12 +142,14 @@ const smtpConfigs = computed({
type Field = { type Field = {
name: string; name: string;
key: keyof ServerConfigs['mailConfigs']['fields']; key: keyof ServerConfigs['mailConfigs']['fields'];
error?: string;
}; };
const smtpConfigFields = reactive<Field[]>([ const smtpConfigFields = reactive<Field[]>([
{ {
name: t('configs.mail_configs.smtp_url'), name: t('configs.mail_configs.smtp_url'),
key: 'mailer_smtp_url', key: 'mailer_smtp_url',
error: t('configs.mail_configs.input_validation'),
}, },
{ {
name: t('configs.mail_configs.address_from'), name: t('configs.mail_configs.address_from'),
@ -215,4 +227,25 @@ const isCheckboxField = (field: Field) => {
const toggleCheckbox = (field: Field) => const toggleCheckbox = (field: Field) =>
((smtpConfigs.value.fields[field.key] as boolean) = ((smtpConfigs.value.fields[field.key] as boolean) =
!smtpConfigs.value.fields[field.key]); !smtpConfigs.value.fields[field.key]);
// Input Validation
const fieldErrors = computed(() => {
const errors: Record<string, boolean> = {};
if (smtpConfigs.value?.fields.mailer_smtp_url) {
const value = smtpConfigs.value.fields.mailer_smtp_url;
errors.mailer_smtp_url =
!value.startsWith('smtp://') && !value.startsWith('smtps://');
}
return errors;
});
const getFieldError = (
fieldKey: keyof ServerConfigs['mailConfigs']['fields']
) => fieldErrors.value[fieldKey];
watch(fieldErrors, (errors) => {
hasInputValidationFailed.value = Object.values(errors).some(Boolean);
});
</script> </script>

View file

@ -1,5 +1,9 @@
import { ref } from 'vue';
import { InfraConfigEnum } from './backend/graphql'; import { InfraConfigEnum } from './backend/graphql';
// Check if any input validation has failed
export const hasInputValidationFailed = ref(false);
export type SsoAuthProviders = 'google' | 'microsoft' | 'github'; export type SsoAuthProviders = 'google' | 'microsoft' | 'github';
export type ServerConfigs = { export type ServerConfigs = {

View file

@ -33,7 +33,7 @@
<div v-if="isConfigUpdated" class="fixed bottom-0 right-0 m-10"> <div v-if="isConfigUpdated" class="fixed bottom-0 right-0 m-10">
<HoppButtonPrimary <HoppButtonPrimary
:label="t('configs.save_changes')" :label="t('configs.save_changes')"
@click="showSaveChangesModal = !showSaveChangesModal" @click="triggerSaveChangesModal"
/> />
</div> </div>
@ -57,6 +57,7 @@ import { computed, ref } from 'vue';
import { useI18n } from '~/composables/i18n'; import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast'; import { useToast } from '~/composables/toast';
import { useConfigHandler } from '~/composables/useConfigHandler'; import { useConfigHandler } from '~/composables/useConfigHandler';
import { hasInputValidationFailed } from '~/helpers/configs';
const t = useI18n(); const t = useI18n();
const toast = useToast(); const toast = useToast();
@ -91,6 +92,17 @@ const areAnyFieldsEmpty = computed(() =>
workingConfigs.value ? AreAnyConfigFieldsEmpty(workingConfigs.value) : false workingConfigs.value ? AreAnyConfigFieldsEmpty(workingConfigs.value) : false
); );
const triggerSaveChangesModal = () => {
if (areAnyFieldsEmpty.value) {
return toast.error(t('configs.input_empty'));
}
if (hasInputValidationFailed.value) {
return toast.error(t('configs.input_validation_error'));
}
showSaveChangesModal.value = true;
};
const restartServer = () => { const restartServer = () => {
if (areAnyFieldsEmpty.value) { if (areAnyFieldsEmpty.value) {
return toast.error(t('configs.input_empty')); return toast.error(t('configs.input_empty'));

View file

@ -53,7 +53,12 @@
<td @click.stop class="flex justify-end mr-10"> <td @click.stop class="flex justify-end mr-10">
<div class="relative"> <div class="relative">
<tippy interactive trigger="click" theme="popover"> <tippy
:key="team.id"
interactive
trigger="click"
theme="popover"
>
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:icon="IconMoreHorizontal" :icon="IconMoreHorizontal"

View file

@ -109,7 +109,12 @@
<td @click.stop class="flex justify-end w-20"> <td @click.stop class="flex justify-end w-20">
<div class="mt-2 mr-5"> <div class="mt-2 mr-5">
<tippy interactive trigger="click" theme="popover"> <tippy
:key="user.uid"
interactive
trigger="click"
theme="popover"
>
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:icon="IconMoreHorizontal" :icon="IconMoreHorizontal"