Browse Source

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

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
Joel Jacob Stephen 3 weeks ago
parent
commit
7f84c52c83

+ 2 - 0
packages/hoppscotch-sh-admin/locales/en.json

@@ -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",

+ 2 - 0
packages/hoppscotch-sh-admin/src/components.d.ts

@@ -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']

+ 35 - 2
packages/hoppscotch-sh-admin/src/components/settings/SmtpConfiguration.vue

@@ -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>

+ 4 - 0
packages/hoppscotch-sh-admin/src/helpers/configs.ts

@@ -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 = {

+ 13 - 1
packages/hoppscotch-sh-admin/src/pages/settings.vue

@@ -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'));

+ 6 - 1
packages/hoppscotch-sh-admin/src/pages/teams/index.vue

@@ -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"

+ 6 - 1
packages/hoppscotch-sh-admin/src/pages/users/index.vue

@@ -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"