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:
Joel Jacob Stephen
2024-06-28 00:45:50 +05:30
committed by GitHub
parent b851d3003c
commit 1d1462df69
11 changed files with 407 additions and 99 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
mutation ToggleSMTP($status: ServiceStatus!) {
toggleSMTP(status: $status)
}

View File

@@ -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,
];

View File

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

View File

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