feat(sh-admin): introducing advanced SMTP configurations and invite links to dashboard (#4087)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
b851d3003c
commit
1d1462df69
@@ -33,12 +33,21 @@
|
||||
},
|
||||
"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",
|
||||
"custom_smtp_configs": "Use Custom SMTP Configurations",
|
||||
"description": " Configure the smtp configurations",
|
||||
"enable_email_auth": "Enable Email based authentication",
|
||||
"enable_smtp": "Enable SMTP",
|
||||
"host": "MAILER HOST",
|
||||
"password": "MAILER PASSWORD",
|
||||
"port": "MAILER PORT",
|
||||
"secure": "MAILER SECURE",
|
||||
"smtp_url": "MAILER SMTP URL",
|
||||
"tls_reject_unauthorized": "TLS REJECT UNAUTHORIZED",
|
||||
"title": "SMTP Configurations",
|
||||
"update_failure": "Failed to update smtp configurations!!"
|
||||
"toggle_failure": "Failed to toggle smtp!!",
|
||||
"update_failure": "Failed to update smtp configurations!!",
|
||||
"user": "MAILER USER"
|
||||
},
|
||||
"reset": {
|
||||
"confirm_reset": "Hoppscotch server must restart to reflect the new changes. Confirm the reset of server configurations?",
|
||||
@@ -210,6 +219,7 @@
|
||||
"admin_id": "Admin ID",
|
||||
"cancel": "Cancel",
|
||||
"confirm_team_deletion": "Confirm deletion of the workspace?",
|
||||
"copy": "Copy",
|
||||
"create_team": "Create Workspace",
|
||||
"date": "Date",
|
||||
"delete_team": "Delete Workspace",
|
||||
@@ -262,6 +272,7 @@
|
||||
"admin_id": "Admin ID",
|
||||
"cancel": "Cancel",
|
||||
"created_on": "Created On",
|
||||
"copy_link": "Copy Link",
|
||||
"date": "Date",
|
||||
"delete": "Delete",
|
||||
"delete_user": "Delete User",
|
||||
|
||||
73
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
73
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
@@ -1,46 +1,45 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
import '@vue/runtime-core';
|
||||
|
||||
export {}
|
||||
export {};
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
AppHeader: typeof import('./components/app/Header.vue')['default']
|
||||
AppLogin: typeof import('./components/app/Login.vue')['default']
|
||||
AppLogout: typeof import('./components/app/Logout.vue')['default']
|
||||
AppModal: typeof import('./components/app/Modal.vue')['default']
|
||||
AppSidebar: typeof import('./components/app/Sidebar.vue')['default']
|
||||
AppToast: typeof import('./components/app/Toast.vue')['default']
|
||||
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default']
|
||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['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']
|
||||
SettingsReset: typeof import('./components/settings/Reset.vue')['default']
|
||||
SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default']
|
||||
SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.vue')['default']
|
||||
SetupDataSharingAndNewsletter: typeof import('./components/setup/DataSharingAndNewsletter.vue')['default']
|
||||
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
|
||||
TeamsDetails: typeof import('./components/teams/Details.vue')['default']
|
||||
TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
|
||||
TeamsMembers: typeof import('./components/teams/Members.vue')['default']
|
||||
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default']
|
||||
Tippy: typeof import('vue-tippy')['Tippy']
|
||||
UiAutoResetIcon: typeof import('./components/ui/AutoResetIcon.vue')['default']
|
||||
UsersDetails: typeof import('./components/users/Details.vue')['default']
|
||||
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
|
||||
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default']
|
||||
AppHeader: typeof import('./components/app/Header.vue')['default'];
|
||||
AppLogin: typeof import('./components/app/Login.vue')['default'];
|
||||
AppLogout: typeof import('./components/app/Logout.vue')['default'];
|
||||
AppModal: typeof import('./components/app/Modal.vue')['default'];
|
||||
AppSidebar: typeof import('./components/app/Sidebar.vue')['default'];
|
||||
AppToast: typeof import('./components/app/Toast.vue')['default'];
|
||||
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default'];
|
||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'];
|
||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'];
|
||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'];
|
||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'];
|
||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'];
|
||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'];
|
||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink'];
|
||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'];
|
||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'];
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['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'];
|
||||
SettingsReset: typeof import('./components/settings/Reset.vue')['default'];
|
||||
SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default'];
|
||||
SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.vue')['default'];
|
||||
SetupDataSharingAndNewsletter: typeof import('./components/setup/DataSharingAndNewsletter.vue')['default'];
|
||||
TeamsAdd: typeof import('./components/teams/Add.vue')['default'];
|
||||
TeamsDetails: typeof import('./components/teams/Details.vue')['default'];
|
||||
TeamsInvite: typeof import('./components/teams/Invite.vue')['default'];
|
||||
TeamsMembers: typeof import('./components/teams/Members.vue')['default'];
|
||||
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default'];
|
||||
Tippy: typeof import('vue-tippy')['Tippy'];
|
||||
UiAutoResetIcon: typeof import('./components/ui/AutoResetIcon.vue')['default'];
|
||||
UsersDetails: typeof import('./components/users/Details.vue')['default'];
|
||||
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'];
|
||||
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
EnableAndDisableSsoDocument,
|
||||
ResetInfraConfigsDocument,
|
||||
ToggleAnalyticsCollectionDocument,
|
||||
ToggleSmtpDocument,
|
||||
UpdateInfraConfigsDocument,
|
||||
} from '~/helpers/backend/graphql';
|
||||
import { ServerConfigs } from '~/helpers/configs';
|
||||
@@ -52,6 +53,7 @@ const updateAllowedAuthProviderMutation = useMutation(
|
||||
const toggleDataSharingMutation = useMutation(
|
||||
ToggleAnalyticsCollectionDocument
|
||||
);
|
||||
const toggleSMTPMutation = useMutation(ToggleSmtpDocument);
|
||||
|
||||
// Mutation handlers
|
||||
const {
|
||||
@@ -59,6 +61,7 @@ const {
|
||||
updateAuthProvider,
|
||||
resetInfraConfigs,
|
||||
updateDataSharingConfigs,
|
||||
toggleSMTPConfigs,
|
||||
} = useConfigHandler(props.workingConfigs);
|
||||
|
||||
// Call relevant mutations on component mount and initiate server restart
|
||||
@@ -111,6 +114,12 @@ onMounted(async () => {
|
||||
if (!dataSharingResult) {
|
||||
return triggerComponentUnMount();
|
||||
}
|
||||
|
||||
const smtpResult = await toggleSMTPConfigs(toggleSMTPMutation);
|
||||
|
||||
if (!smtpResult) {
|
||||
return triggerComponentUnMount();
|
||||
}
|
||||
}
|
||||
|
||||
restart.value = true;
|
||||
|
||||
@@ -22,30 +22,66 @@
|
||||
:on="smtpConfigs.enabled"
|
||||
@change="smtpConfigs.enabled = !smtpConfigs.enabled"
|
||||
>
|
||||
{{ t('configs.mail_configs.enable') }}
|
||||
{{ t('configs.mail_configs.enable_smtp') }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
|
||||
<div v-if="smtpConfigs.enabled" class="ml-12">
|
||||
<div class="flex flex-col items-start gap-5">
|
||||
<HoppSmartCheckbox
|
||||
:on="smtpConfigs.fields.email_auth"
|
||||
@change="
|
||||
smtpConfigs.fields.email_auth = !smtpConfigs.fields.email_auth
|
||||
"
|
||||
>
|
||||
{{ t('configs.mail_configs.enable_email_auth') }}
|
||||
</HoppSmartCheckbox>
|
||||
|
||||
<HoppSmartCheckbox
|
||||
:on="smtpConfigs.fields.mailer_use_custom_configs"
|
||||
:title="t('configs.mail_configs.custom_smtp_configs')"
|
||||
@change="
|
||||
smtpConfigs.fields.mailer_use_custom_configs =
|
||||
!smtpConfigs.fields.mailer_use_custom_configs
|
||||
"
|
||||
>
|
||||
{{ t('configs.mail_configs.custom_smtp_configs') }}
|
||||
</HoppSmartCheckbox>
|
||||
</div>
|
||||
<div
|
||||
v-for="field in smtpConfigFields"
|
||||
:key="field.key"
|
||||
class="mt-5"
|
||||
class="mt-5 ml-12"
|
||||
>
|
||||
<label>{{ field.name }}</label>
|
||||
<span class="flex max-w-lg">
|
||||
<HoppSmartInput
|
||||
v-model="smtpConfigs.fields[field.key]"
|
||||
:type="isMasked(field.key) ? 'password' : 'text'"
|
||||
:autofocus="false"
|
||||
class="!my-2 !bg-primaryLight flex-1"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="isMasked(field.key) ? IconEye : IconEyeOff"
|
||||
class="bg-primaryLight h-9 mt-2"
|
||||
@click="toggleMask(field.key)"
|
||||
/>
|
||||
</span>
|
||||
<div v-if="fieldCondition(field)">
|
||||
<div
|
||||
v-if="isCheckboxField(field)"
|
||||
class="flex flex-col items-start gap-5"
|
||||
>
|
||||
<HoppSmartCheckbox
|
||||
:on="Boolean(smtpConfigs.fields[field.key])"
|
||||
@change="toggleCheckbox(field)"
|
||||
>
|
||||
{{ field.name }}
|
||||
</HoppSmartCheckbox>
|
||||
</div>
|
||||
<span v-else>
|
||||
<label>{{ field.name }}</label>
|
||||
<span class="flex max-w-lg">
|
||||
<HoppSmartInput
|
||||
v-model="smtpConfigs.fields[field.key]"
|
||||
:type="isMasked(field.key) ? 'password' : 'text'"
|
||||
:autofocus="false"
|
||||
class="!my-2 !bg-primaryLight flex-1"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="isMasked(field.key) ? IconEye : IconEyeOff"
|
||||
class="bg-primaryLight h-9 mt-2"
|
||||
@click="toggleMask(field.key)"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,13 +127,47 @@ type Field = {
|
||||
};
|
||||
|
||||
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' },
|
||||
{
|
||||
name: t('configs.mail_configs.smtp_url'),
|
||||
key: 'mailer_smtp_url',
|
||||
},
|
||||
{
|
||||
name: t('configs.mail_configs.address_from'),
|
||||
key: 'mailer_from_address',
|
||||
},
|
||||
{
|
||||
name: t('configs.mail_configs.host'),
|
||||
key: 'mailer_smtp_host',
|
||||
},
|
||||
{
|
||||
name: t('configs.mail_configs.port'),
|
||||
key: 'mailer_smtp_port',
|
||||
},
|
||||
{
|
||||
name: t('configs.mail_configs.user'),
|
||||
key: 'mailer_smtp_user',
|
||||
},
|
||||
{
|
||||
name: t('configs.mail_configs.password'),
|
||||
key: 'mailer_smtp_password',
|
||||
},
|
||||
{
|
||||
name: t('configs.mail_configs.secure'),
|
||||
key: 'mailer_smtp_secure',
|
||||
},
|
||||
{
|
||||
name: t('configs.mail_configs.tls_reject_unauthorized'),
|
||||
key: 'mailer_tls_reject_unauthorized',
|
||||
},
|
||||
]);
|
||||
|
||||
const maskState = reactive<Record<string, boolean>>({
|
||||
mailer_smtp_url: true,
|
||||
mailer_from_address: true,
|
||||
mailer_smtp_host: true,
|
||||
mailer_smtp_port: true,
|
||||
mailer_smtp_user: true,
|
||||
mailer_smtp_password: true,
|
||||
});
|
||||
|
||||
const toggleMask = (fieldKey: keyof ServerConfigs['mailConfigs']['fields']) => {
|
||||
@@ -106,4 +176,35 @@ const toggleMask = (fieldKey: keyof ServerConfigs['mailConfigs']['fields']) => {
|
||||
|
||||
const isMasked = (fieldKey: keyof ServerConfigs['mailConfigs']['fields']) =>
|
||||
maskState[fieldKey];
|
||||
|
||||
const fieldCondition = (field: Field) => {
|
||||
const advancedFields = [
|
||||
'mailer_smtp_host',
|
||||
'mailer_smtp_port',
|
||||
'mailer_smtp_user',
|
||||
'mailer_smtp_password',
|
||||
'mailer_smtp_secure',
|
||||
'mailer_tls_reject_unauthorized',
|
||||
];
|
||||
const basicFields = ['mailer_smtp_url'];
|
||||
|
||||
if (field.key === 'mailer_from_address') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (smtpConfigs.value.fields.mailer_use_custom_configs) {
|
||||
return (
|
||||
!basicFields.includes(field.key) && advancedFields.includes(field.key)
|
||||
);
|
||||
} else return basicFields.includes(field.key);
|
||||
};
|
||||
|
||||
const isCheckboxField = (field: Field) => {
|
||||
const checkboxKeys = ['mailer_smtp_secure', 'mailer_tls_reject_unauthorized'];
|
||||
return checkboxKeys.includes(field.key);
|
||||
};
|
||||
|
||||
const toggleCheckbox = (field: Field) =>
|
||||
((smtpConfigs.value.fields[field.key] as boolean) =
|
||||
!smtpConfigs.value.fields[field.key]);
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div class="border rounded divide-y divide-dividerLight border-divider my-8">
|
||||
<div
|
||||
class="border rounded divide-y divide-dividerLight border-divider mx-4 my-8"
|
||||
>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="team && pendingInvites?.length === 0"
|
||||
text="No pending invites"
|
||||
@@ -26,16 +28,22 @@
|
||||
:value="invitee.inviteeRole"
|
||||
readonly
|
||||
/>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('teams.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
:loading="isLoadingIndex === index"
|
||||
@click="removeInvitee(invitee.id, index)"
|
||||
/>
|
||||
</div>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('teams.copy')"
|
||||
:icon="IconCopy"
|
||||
color="white"
|
||||
:loading="isLoadingIndex === index"
|
||||
@click="copyInviteLink(invitee.id)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('teams.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
:loading="isLoadingIndex === index"
|
||||
@click="removeInvitee(invitee.id, index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,6 +59,8 @@ import {
|
||||
RevokeTeamInvitationDocument,
|
||||
TeamInfoQuery,
|
||||
} from '~/helpers/backend/graphql';
|
||||
import { copyToClipboard } from '~/helpers/utils/clipboard';
|
||||
import IconCopy from '~icons/lucide/copy';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
|
||||
const t = useI18n();
|
||||
@@ -96,4 +106,11 @@ const removeInvitee = async (id: string, index: number) => {
|
||||
}
|
||||
isLoadingIndex.value = null;
|
||||
};
|
||||
|
||||
const baseURL = import.meta.env.VITE_BASE_URL ?? '';
|
||||
|
||||
const copyInviteLink = (inviteID: string) => {
|
||||
copyToClipboard(`${baseURL}/join-team?id=${inviteID}`);
|
||||
toast.success(t('state.copied_to_clipboard'));
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
v-model="email"
|
||||
:label="t('users.email_address')"
|
||||
input-styles="floating-input"
|
||||
@submit="sendInvite"
|
||||
@submit="emit('send-invite', email)"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('users.send_invite')"
|
||||
@click="sendInvite"
|
||||
@click="emit('send-invite', email)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('users.cancel')"
|
||||
@@ -25,6 +25,12 @@
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
<HoppButtonSecondary
|
||||
:label="t('users.copy_link')"
|
||||
outline
|
||||
filled
|
||||
@click="emit('copy-invite-link', email)"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
@@ -32,26 +38,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
|
||||
const t = useI18n();
|
||||
const toast = useToast();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'hide-modal'): void;
|
||||
(event: 'send-invite', email: string): void;
|
||||
(event: 'copy-invite-link', email: string): void;
|
||||
}>();
|
||||
|
||||
const email = ref('');
|
||||
|
||||
const sendInvite = () => {
|
||||
if (email.value.trim() === '') {
|
||||
toast.error(t('users.valid_email'));
|
||||
return;
|
||||
}
|
||||
emit('send-invite', email.value);
|
||||
};
|
||||
|
||||
const hideModal = () => {
|
||||
emit('hide-modal');
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AnyVariables, UseMutationResponse } from '@urql/vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import {
|
||||
AllowedAuthProvidersDocument,
|
||||
@@ -14,10 +13,12 @@ import {
|
||||
ResetInfraConfigsMutation,
|
||||
ServiceStatus,
|
||||
ToggleAnalyticsCollectionMutation,
|
||||
ToggleSmtpMutation,
|
||||
UpdateInfraConfigsMutation,
|
||||
} from '~/helpers/backend/graphql';
|
||||
import {
|
||||
ALL_CONFIGS,
|
||||
CUSTOM_MAIL_CONFIGS,
|
||||
ConfigSection,
|
||||
ConfigTransform,
|
||||
GITHUB_CONFIGS,
|
||||
@@ -114,10 +115,24 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||
},
|
||||
mailConfigs: {
|
||||
name: 'email',
|
||||
enabled: allowedAuthProviders.value.includes(AuthProvider.Email),
|
||||
enabled: getFieldValue(InfraConfigEnum.MailerSmtpEnable) === 'true',
|
||||
fields: {
|
||||
email_auth: allowedAuthProviders.value.includes(AuthProvider.Email),
|
||||
mailer_smtp_url: getFieldValue(InfraConfigEnum.MailerSmtpUrl),
|
||||
mailer_from_address: getFieldValue(InfraConfigEnum.MailerAddressFrom),
|
||||
mailer_smtp_host: getFieldValue(InfraConfigEnum.MailerSmtpHost),
|
||||
mailer_smtp_port: getFieldValue(InfraConfigEnum.MailerSmtpPort),
|
||||
mailer_smtp_user: getFieldValue(InfraConfigEnum.MailerSmtpUser),
|
||||
mailer_smtp_password: getFieldValue(
|
||||
InfraConfigEnum.MailerSmtpPassword
|
||||
),
|
||||
mailer_smtp_secure:
|
||||
getFieldValue(InfraConfigEnum.MailerSmtpSecure) === 'true',
|
||||
mailer_tls_reject_unauthorized:
|
||||
getFieldValue(InfraConfigEnum.MailerTlsRejectUnauthorized) ===
|
||||
'true',
|
||||
mailer_use_custom_configs:
|
||||
getFieldValue(InfraConfigEnum.MailerUseCustomConfigs) === 'true',
|
||||
},
|
||||
},
|
||||
dataSharingConfigs: {
|
||||
@@ -133,11 +148,19 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||
workingConfigs.value = cloneDeep(currentConfigs.value);
|
||||
});
|
||||
|
||||
// Check if custom mail config is enabled
|
||||
const isCustomMailConfigEnabled =
|
||||
updatedConfigs?.mailConfigs.fields.mailer_use_custom_configs;
|
||||
|
||||
/*
|
||||
Check if any of the config fields are empty
|
||||
*/
|
||||
|
||||
const isFieldEmpty = (field: string) => field.trim() === '';
|
||||
const isFieldEmpty = (field: string | boolean) => {
|
||||
if (typeof field === 'boolean') {
|
||||
return false;
|
||||
}
|
||||
return field.trim() === '';
|
||||
};
|
||||
|
||||
const AreAnyConfigFieldsEmpty = (config: ServerConfigs): boolean => {
|
||||
const sections: Array<ConfigSection> = [
|
||||
@@ -147,12 +170,56 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||
config.mailConfigs,
|
||||
];
|
||||
|
||||
return sections.some(
|
||||
(section) =>
|
||||
const hasSectionWithEmptyFields = sections.some((section) => {
|
||||
if (
|
||||
section.name === 'email' &&
|
||||
!section.fields.mailer_use_custom_configs
|
||||
) {
|
||||
return (
|
||||
section.enabled &&
|
||||
Object.entries(section.fields).some(
|
||||
([key, value]) =>
|
||||
isFieldEmpty(value) &&
|
||||
key !== 'mailer_smtp_host' &&
|
||||
key !== 'mailer_smtp_port' &&
|
||||
key !== 'mailer_smtp_user' &&
|
||||
key !== 'mailer_smtp_password'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
section.enabled && Object.values(section.fields).some(isFieldEmpty)
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
return hasSectionWithEmptyFields;
|
||||
};
|
||||
|
||||
// Extract the mail config fields (excluding the custom mail config fields)
|
||||
const mailConfigFields = Object.fromEntries(
|
||||
Object.entries(updatedConfigs?.mailConfigs.fields ?? {}).filter(([key]) => {
|
||||
if (isCustomMailConfigEnabled) {
|
||||
return MAIL_CONFIGS.some(
|
||||
(x) =>
|
||||
x.key === key &&
|
||||
key !== 'mailer_smtp_url' &&
|
||||
key !== 'mailer_smtp_enabled'
|
||||
);
|
||||
} else
|
||||
return MAIL_CONFIGS.some(
|
||||
(x) => x.key === key && key !== 'mailer_smtp_enabled'
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// Extract the custom mail config fields
|
||||
const customMailConfigFields = Object.fromEntries(
|
||||
Object.entries(updatedConfigs?.mailConfigs.fields ?? {}).filter(([key]) =>
|
||||
CUSTOM_MAIL_CONFIGS.some((x) => x.key === key)
|
||||
)
|
||||
);
|
||||
|
||||
// Transforming the working configs back into the format required by the mutations
|
||||
const transformInfraConfigs = () => {
|
||||
const updatedWorkingConfigs: ConfigTransform[] = [
|
||||
@@ -174,7 +241,12 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||
{
|
||||
config: MAIL_CONFIGS,
|
||||
enabled: updatedConfigs?.mailConfigs.enabled,
|
||||
fields: updatedConfigs?.mailConfigs.fields,
|
||||
fields: mailConfigFields,
|
||||
},
|
||||
{
|
||||
config: CUSTOM_MAIL_CONFIGS,
|
||||
enabled: isCustomMailConfigEnabled,
|
||||
fields: customMailConfigFields,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -182,7 +254,10 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||
|
||||
updatedWorkingConfigs.forEach(({ config, enabled, fields }) => {
|
||||
config.forEach(({ name, key }) => {
|
||||
if (enabled && fields) {
|
||||
if (name === 'MAILER_SMTP_ENABLE') return;
|
||||
else if (isCustomMailConfigEnabled && name === 'MAILER_SMTP_URL')
|
||||
return;
|
||||
else if (enabled && fields) {
|
||||
const value =
|
||||
typeof fields === 'string' ? fields : String(fields[key]);
|
||||
transformedConfigs.push({ name, value });
|
||||
@@ -239,7 +314,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||
},
|
||||
{
|
||||
provider: AuthProvider.Email,
|
||||
status: updatedConfigs?.mailConfigs.enabled
|
||||
status: updatedConfigs?.mailConfigs.fields.email_auth
|
||||
? ServiceStatus.Enable
|
||||
: ServiceStatus.Disable,
|
||||
},
|
||||
@@ -281,6 +356,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||
'configs.reset.failure'
|
||||
);
|
||||
|
||||
// Toggle Data Sharing
|
||||
const updateDataSharingConfigs = (
|
||||
toggleDataSharingMutation: UseMutationResponse<ToggleAnalyticsCollectionMutation>
|
||||
) =>
|
||||
@@ -294,11 +370,26 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||
'configs.data_sharing.update_failure'
|
||||
);
|
||||
|
||||
// Toggle SMTP
|
||||
const toggleSMTPConfigs = (
|
||||
toggleSMTP: UseMutationResponse<ToggleSmtpMutation>
|
||||
) =>
|
||||
executeMutation(
|
||||
toggleSMTP,
|
||||
{
|
||||
status: updatedConfigs?.mailConfigs.enabled
|
||||
? ServiceStatus.Enable
|
||||
: ServiceStatus.Disable,
|
||||
},
|
||||
'configs.mail_configs.toggle_failure'
|
||||
);
|
||||
|
||||
return {
|
||||
currentConfigs,
|
||||
workingConfigs,
|
||||
updateAuthProvider,
|
||||
updateDataSharingConfigs,
|
||||
toggleSMTPConfigs,
|
||||
updateInfraConfigs,
|
||||
resetInfraConfigs,
|
||||
fetchingInfraConfigs,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation ToggleSMTP($status: ServiceStatus!) {
|
||||
toggleSMTP(status: $status)
|
||||
}
|
||||
@@ -41,8 +41,16 @@ export type ServerConfigs = {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
fields: {
|
||||
email_auth: boolean;
|
||||
mailer_smtp_url: string;
|
||||
mailer_from_address: string;
|
||||
mailer_smtp_host: string;
|
||||
mailer_smtp_port: string;
|
||||
mailer_smtp_user: string;
|
||||
mailer_smtp_password: string;
|
||||
mailer_smtp_secure: boolean;
|
||||
mailer_tls_reject_unauthorized: boolean;
|
||||
mailer_use_custom_configs: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -64,8 +72,9 @@ export type ConfigTransform = {
|
||||
};
|
||||
|
||||
export type ConfigSection = {
|
||||
name: SsoAuthProviders | string;
|
||||
enabled: boolean;
|
||||
fields: Record<string, string>;
|
||||
fields: Record<string, string | boolean>;
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
@@ -143,6 +152,41 @@ export const MAIL_CONFIGS: Config[] = [
|
||||
name: InfraConfigEnum.MailerAddressFrom,
|
||||
key: 'mailer_from_address',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerSmtpEnable,
|
||||
key: 'mailer_smtp_enabled',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerUseCustomConfigs,
|
||||
key: 'mailer_use_custom_configs',
|
||||
},
|
||||
];
|
||||
|
||||
export const CUSTOM_MAIL_CONFIGS: Config[] = [
|
||||
{
|
||||
name: InfraConfigEnum.MailerSmtpHost,
|
||||
key: 'mailer_smtp_host',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerSmtpPort,
|
||||
key: 'mailer_smtp_port',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerSmtpUser,
|
||||
key: 'mailer_smtp_user',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerSmtpPassword,
|
||||
key: 'mailer_smtp_password',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerSmtpSecure,
|
||||
key: 'mailer_smtp_secure',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerTlsRejectUnauthorized,
|
||||
key: 'mailer_tls_reject_unauthorized',
|
||||
},
|
||||
];
|
||||
|
||||
const DATA_SHARING_CONFIGS: Omit<Config, 'key'>[] = [
|
||||
@@ -156,5 +200,6 @@ export const ALL_CONFIGS = [
|
||||
MICROSOFT_CONFIGS,
|
||||
GITHUB_CONFIGS,
|
||||
MAIL_CONFIGS,
|
||||
CUSTOM_MAIL_CONFIGS,
|
||||
DATA_SHARING_CONFIGS,
|
||||
];
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
v-if="showInviteUserModal"
|
||||
@hide-modal="showInviteUserModal = false"
|
||||
@send-invite="sendInvite"
|
||||
@copy-invite-link="copyInviteLink"
|
||||
/>
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmUsersToAdmin"
|
||||
@@ -262,6 +263,7 @@ import {
|
||||
} from '~/helpers/backend/graphql';
|
||||
import { getCompiledErrorMessage } from '~/helpers/errors';
|
||||
import { handleUserDeletion } from '~/helpers/userManagement';
|
||||
import { copyToClipboard } from '~/helpers/utils/clipboard';
|
||||
import IconCheck from '~icons/lucide/check';
|
||||
import IconLeft from '~icons/lucide/chevron-left';
|
||||
import IconRight from '~icons/lucide/chevron-right';
|
||||
@@ -451,11 +453,16 @@ const showInviteUserModal = ref(false);
|
||||
const sendInvitation = useMutation(InviteNewUserDocument);
|
||||
|
||||
const sendInvite = async (email: string) => {
|
||||
if (!email.trim()) {
|
||||
const trimmedEmail = email.trim();
|
||||
if (!trimmedEmail) {
|
||||
toast.error(t('state.invalid_email'));
|
||||
return;
|
||||
return false;
|
||||
} else if (trimmedEmail === '') {
|
||||
toast.error(t('users.valid_email'));
|
||||
return false;
|
||||
}
|
||||
const variables = { inviteeEmail: email.trim() };
|
||||
|
||||
const variables = { inviteeEmail: trimmedEmail };
|
||||
const result = await sendInvitation.executeMutation(variables);
|
||||
if (result.error) {
|
||||
const { message } = result.error;
|
||||
@@ -464,12 +471,23 @@ const sendInvite = async (email: string) => {
|
||||
compiledErrorMessage
|
||||
? toast.error(t(compiledErrorMessage))
|
||||
: toast.error(t('state.email_failure'));
|
||||
|
||||
return false;
|
||||
} else {
|
||||
toast.success(t('state.email_success'));
|
||||
showInviteUserModal.value = false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const copyInviteLink = async (email: string) => {
|
||||
const result = await sendInvite(email);
|
||||
if (!result) return;
|
||||
const baseURL = import.meta.env.VITE_BASE_URL ?? '';
|
||||
copyToClipboard(baseURL);
|
||||
toast.success(t('state.copied_to_clipboard'));
|
||||
};
|
||||
|
||||
// Make Multiple Users Admin
|
||||
const confirmUsersToAdmin = ref(false);
|
||||
const usersToAdminUID = ref<string | null>(null);
|
||||
|
||||
@@ -6,9 +6,18 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-bold text-accentContrast pt-6 pb-4">
|
||||
{{ t('users.pending_invites') }}
|
||||
</h3>
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-bold text-accentContrast pt-6 pb-4">
|
||||
{{ t('users.pending_invites') }}
|
||||
</h3>
|
||||
|
||||
<HoppButtonSecondary
|
||||
:label="t('users.copy_link')"
|
||||
outline
|
||||
filled
|
||||
@click="copyInviteLink"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="relative py-2 overflow-x-auto">
|
||||
@@ -107,6 +116,7 @@ import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { copyToClipboard } from '~/helpers/utils/clipboard';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import {
|
||||
InvitedUsersDocument,
|
||||
@@ -185,4 +195,11 @@ const deleteInvitation = async (email: string | null) => {
|
||||
confirmDeletion.value = false;
|
||||
inviteToBeDeleted.value = null;
|
||||
};
|
||||
|
||||
const baseURL = import.meta.env.VITE_BASE_URL ?? '';
|
||||
|
||||
const copyInviteLink = () => {
|
||||
copyToClipboard(baseURL);
|
||||
toast.success(t('state.copied_to_clipboard'));
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user