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?",
"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": {
"title": "Data Sharing",
"description": "Help improve Hoppscotch by sharing anonymous data",
@ -48,6 +49,7 @@
"enable_email_auth": "Enable Email based authentication",
"enable_smtp": "Enable SMTP",
"host": "MAILER HOST",
"input_validation": "SMTP URL should start with smtp(s)://",
"password": "MAILER PASSWORD",
"port": "MAILER PORT",
"secure": "MAILER SECURE",

View file

@ -40,8 +40,10 @@ declare module 'vue' {
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
IconLucideInfo: typeof import('~icons/lucide/info')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUser: typeof import('~icons/lucide/user')['default']
IconLucideX: typeof import('~icons/lucide/x')['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']

View file

@ -87,6 +87,16 @@
@click="toggleMask(field.key)"
/>
</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>
</div>
</div>
@ -99,9 +109,9 @@
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import { computed, reactive } from 'vue';
import { computed, reactive, watch } from 'vue';
import { useI18n } from '~/composables/i18n';
import { ServerConfigs } from '~/helpers/configs';
import { hasInputValidationFailed, ServerConfigs } from '~/helpers/configs';
import IconEye from '~icons/lucide/eye';
import IconEyeOff from '~icons/lucide/eye-off';
import IconHelpCircle from '~icons/lucide/help-circle';
@ -132,12 +142,14 @@ const smtpConfigs = computed({
type Field = {
name: string;
key: keyof ServerConfigs['mailConfigs']['fields'];
error?: string;
};
const smtpConfigFields = reactive<Field[]>([
{
name: t('configs.mail_configs.smtp_url'),
key: 'mailer_smtp_url',
error: t('configs.mail_configs.input_validation'),
},
{
name: t('configs.mail_configs.address_from'),
@ -215,4 +227,25 @@ const isCheckboxField = (field: Field) => {
const toggleCheckbox = (field: Field) =>
((smtpConfigs.value.fields[field.key] as boolean) =
!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>

View file

@ -1,5 +1,9 @@
import { ref } from 'vue';
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 ServerConfigs = {

View file

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

View file

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

View file

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