feat: add SMTP OAuth2 authentication support (#6141)
Co-authored-by: nivedin <nivedinp@gmail.com> Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
7be31a2986
commit
eb801889ba
17 changed files with 857 additions and 189 deletions
|
|
@ -114,6 +114,31 @@ export class SaveOnboardingConfigRequest {
|
|||
@IsOptional()
|
||||
@IsString()
|
||||
[InfraConfigEnum.MAILER_SMTP_IGNORE_TLS]: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
[InfraConfigEnum.MAILER_SMTP_AUTH_TYPE]: string;
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
[InfraConfigEnum.MAILER_SMTP_OAUTH2_USER]: string;
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
[InfraConfigEnum.MAILER_SMTP_OAUTH2_CLIENT_ID]: string;
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
[InfraConfigEnum.MAILER_SMTP_OAUTH2_CLIENT_SECRET]: string;
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
[InfraConfigEnum.MAILER_SMTP_OAUTH2_REFRESH_TOKEN]: string;
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
[InfraConfigEnum.MAILER_SMTP_OAUTH2_ACCESS_URL]: string;
|
||||
}
|
||||
|
||||
export class SaveOnboardingConfigResponse {
|
||||
|
|
@ -204,4 +229,23 @@ export class GetOnboardingConfigResponse {
|
|||
@ApiProperty()
|
||||
@Expose()
|
||||
[InfraConfigEnum.MAILER_SMTP_IGNORE_TLS]: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
[InfraConfigEnum.MAILER_SMTP_AUTH_TYPE]: string;
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
[InfraConfigEnum.MAILER_SMTP_OAUTH2_USER]: string;
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
[InfraConfigEnum.MAILER_SMTP_OAUTH2_CLIENT_ID]: string;
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
[InfraConfigEnum.MAILER_SMTP_OAUTH2_CLIENT_SECRET]: string;
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
[InfraConfigEnum.MAILER_SMTP_OAUTH2_REFRESH_TOKEN]: string;
|
||||
@ApiProperty()
|
||||
@Expose()
|
||||
[InfraConfigEnum.MAILER_SMTP_OAUTH2_ACCESS_URL]: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { AuthProvider } from 'src/auth/helper';
|
|||
import { ENV_INVALID_DATA_ENCRYPTION_KEY } from 'src/errors';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||
import { SMTPAuthType } from 'src/mailer/helper';
|
||||
import { decrypt, encrypt } from 'src/utils';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
|
|
@ -243,6 +244,36 @@ export async function getDefaultInfraConfigs(): Promise<DefaultInfraConfig[]> {
|
|||
value: 'false',
|
||||
isEncrypted: false,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MAILER_SMTP_AUTH_TYPE,
|
||||
value: SMTPAuthType.LOGIN,
|
||||
isEncrypted: false,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MAILER_SMTP_OAUTH2_USER,
|
||||
value: null,
|
||||
isEncrypted: false,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MAILER_SMTP_OAUTH2_CLIENT_ID,
|
||||
value: null,
|
||||
isEncrypted: true,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MAILER_SMTP_OAUTH2_CLIENT_SECRET,
|
||||
value: null,
|
||||
isEncrypted: true,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MAILER_SMTP_OAUTH2_REFRESH_TOKEN,
|
||||
value: null,
|
||||
isEncrypted: true,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MAILER_SMTP_OAUTH2_ACCESS_URL,
|
||||
value: null,
|
||||
isEncrypted: false,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||
value: null,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
|||
import { InfraConfigService } from './infra-config.service';
|
||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||
import {
|
||||
INFRA_CONFIG_INVALID_INPUT,
|
||||
INFRA_CONFIG_NOT_FOUND,
|
||||
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
|
||||
INFRA_CONFIG_UPDATE_FAILED,
|
||||
|
|
@ -292,4 +293,58 @@ describe('InfraConfigService', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEnvValues', () => {
|
||||
describe('MAILER_SMTP_AUTH_TYPE', () => {
|
||||
it('should accept an empty value (defaults to login at runtime)', () => {
|
||||
const result = infraConfigService.validateEnvValues([
|
||||
{ name: InfraConfigEnum.MAILER_SMTP_AUTH_TYPE, value: '' },
|
||||
]);
|
||||
expect(result).toEqualRight(true);
|
||||
});
|
||||
|
||||
it('should accept a known auth type', () => {
|
||||
const result = infraConfigService.validateEnvValues([
|
||||
{ name: InfraConfigEnum.MAILER_SMTP_AUTH_TYPE, value: 'oauth2' },
|
||||
]);
|
||||
expect(result).toEqualRight(true);
|
||||
});
|
||||
|
||||
it('should reject an unknown auth type', () => {
|
||||
const result = infraConfigService.validateEnvValues([
|
||||
{ name: InfraConfigEnum.MAILER_SMTP_AUTH_TYPE, value: 'kerberos' },
|
||||
]);
|
||||
expect(result).toEqualLeft(INFRA_CONFIG_INVALID_INPUT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MAILER_SMTP_OAUTH2_ACCESS_URL', () => {
|
||||
it('should accept an empty value', () => {
|
||||
const result = infraConfigService.validateEnvValues([
|
||||
{ name: InfraConfigEnum.MAILER_SMTP_OAUTH2_ACCESS_URL, value: '' },
|
||||
]);
|
||||
expect(result).toEqualRight(true);
|
||||
});
|
||||
|
||||
it('should accept a valid HTTPS URL', () => {
|
||||
const result = infraConfigService.validateEnvValues([
|
||||
{
|
||||
name: InfraConfigEnum.MAILER_SMTP_OAUTH2_ACCESS_URL,
|
||||
value: 'https://oauth2.googleapis.com/token',
|
||||
},
|
||||
]);
|
||||
expect(result).toEqualRight(true);
|
||||
});
|
||||
|
||||
it('should reject a malformed URL', () => {
|
||||
const result = infraConfigService.validateEnvValues([
|
||||
{
|
||||
name: InfraConfigEnum.MAILER_SMTP_OAUTH2_ACCESS_URL,
|
||||
value: 'not-a-url',
|
||||
},
|
||||
]);
|
||||
expect(result).toEqualLeft(INFRA_CONFIG_INVALID_INPUT);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
|||
import { InfraConfig as DBInfraConfig } from 'src/generated/prisma/client';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||
import { SMTPAuthType } from 'src/mailer/helper';
|
||||
import {
|
||||
AUTH_PROVIDER_NOT_SPECIFIED,
|
||||
DATABASE_TABLE_NOT_EXIST,
|
||||
|
|
@ -736,6 +737,18 @@ export class InfraConfigService implements OnModuleInit, OnModuleDestroy {
|
|||
if (value !== 'true' && value !== 'false') return fail();
|
||||
break;
|
||||
|
||||
case InfraConfigEnum.MAILER_SMTP_AUTH_TYPE:
|
||||
if (
|
||||
value &&
|
||||
!Object.values(SMTPAuthType).includes(value as SMTPAuthType)
|
||||
)
|
||||
return fail();
|
||||
break;
|
||||
|
||||
case InfraConfigEnum.MAILER_SMTP_OAUTH2_ACCESS_URL:
|
||||
if (value && !validateUrl(value)) return fail();
|
||||
break;
|
||||
|
||||
case InfraConfigEnum.MAILER_SMTP_URL:
|
||||
if (!validateSMTPUrl(value)) return fail();
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { TransportType } from '@nestjs-modules/mailer/dist/interfaces/mailer-options.interface';
|
||||
import type SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
import { MAILER_SMTP_URL_UNDEFINED } from 'src/errors';
|
||||
import { throwErr } from 'src/utils';
|
||||
|
||||
export enum SMTPAuthType {
|
||||
LOGIN = 'login',
|
||||
OAUTH2 = 'oauth2',
|
||||
}
|
||||
|
||||
function isSMTPCustomConfigsEnabled(value) {
|
||||
return value === 'true';
|
||||
}
|
||||
|
|
@ -21,26 +27,52 @@ export function getTransportOption(env): TransportType {
|
|||
} else {
|
||||
console.log('Using advanced mailer configuration');
|
||||
|
||||
const smtpUser = env.INFRA.MAILER_SMTP_USER?.trim() || undefined;
|
||||
const smtpPass = env.INFRA.MAILER_SMTP_PASSWORD?.trim() || undefined;
|
||||
const authType = env.INFRA.MAILER_SMTP_AUTH_TYPE?.trim();
|
||||
|
||||
// Both credentials must be provided together or both omitted
|
||||
const hasUser = !!smtpUser;
|
||||
const hasPass = !!smtpPass;
|
||||
if (hasUser !== hasPass) {
|
||||
throw new Error(
|
||||
'SMTP auth requires both MAILER_SMTP_USER and MAILER_SMTP_PASSWORD. Provide both or leave both empty for unauthenticated relay.',
|
||||
);
|
||||
let auth: SMTPConnection.AuthenticationType | undefined;
|
||||
|
||||
if (authType === SMTPAuthType.OAUTH2) {
|
||||
const oauth2User = env.INFRA.MAILER_SMTP_OAUTH2_USER?.trim() || undefined;
|
||||
const oauth2ClientId =
|
||||
env.INFRA.MAILER_SMTP_OAUTH2_CLIENT_ID?.trim() || undefined;
|
||||
const oauth2ClientSecret =
|
||||
env.INFRA.MAILER_SMTP_OAUTH2_CLIENT_SECRET?.trim() || undefined;
|
||||
const oauth2RefreshToken =
|
||||
env.INFRA.MAILER_SMTP_OAUTH2_REFRESH_TOKEN?.trim() || undefined;
|
||||
const oauth2AccessUrl =
|
||||
env.INFRA.MAILER_SMTP_OAUTH2_ACCESS_URL?.trim() || undefined;
|
||||
|
||||
auth = {
|
||||
type: SMTPAuthType.OAUTH2,
|
||||
user: oauth2User,
|
||||
clientId: oauth2ClientId,
|
||||
clientSecret: oauth2ClientSecret,
|
||||
refreshToken: oauth2RefreshToken,
|
||||
accessUrl: oauth2AccessUrl,
|
||||
};
|
||||
} else {
|
||||
const smtpUser = env.INFRA.MAILER_SMTP_USER?.trim() || undefined;
|
||||
const smtpPass = env.INFRA.MAILER_SMTP_PASSWORD?.trim() || undefined;
|
||||
|
||||
const hasUser = !!smtpUser;
|
||||
const hasPass = !!smtpPass;
|
||||
if (hasUser !== hasPass) {
|
||||
throw new Error(
|
||||
'SMTP auth requires both MAILER_SMTP_USER and MAILER_SMTP_PASSWORD. Provide both or leave both empty for unauthenticated relay.',
|
||||
);
|
||||
}
|
||||
|
||||
auth =
|
||||
smtpUser && smtpPass
|
||||
? { type: SMTPAuthType.LOGIN, user: smtpUser, pass: smtpPass }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const auth =
|
||||
smtpUser && smtpPass ? { user: smtpUser, pass: smtpPass } : undefined;
|
||||
|
||||
return {
|
||||
host: env.INFRA.MAILER_SMTP_HOST,
|
||||
port: +env.INFRA.MAILER_SMTP_PORT,
|
||||
secure: env.INFRA.MAILER_SMTP_SECURE === 'true',
|
||||
...(auth && { auth }),
|
||||
auth,
|
||||
ignoreTLS: env.INFRA.MAILER_SMTP_IGNORE_TLS === 'true',
|
||||
tls: {
|
||||
rejectUnauthorized: env.INFRA.MAILER_TLS_REJECT_UNAUTHORIZED === 'true',
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ export enum InfraConfigEnum {
|
|||
MAILER_TLS_REJECT_UNAUTHORIZED = 'MAILER_TLS_REJECT_UNAUTHORIZED',
|
||||
MAILER_SMTP_IGNORE_TLS = 'MAILER_SMTP_IGNORE_TLS',
|
||||
|
||||
MAILER_SMTP_AUTH_TYPE = 'MAILER_SMTP_AUTH_TYPE',
|
||||
MAILER_SMTP_OAUTH2_USER = 'MAILER_SMTP_OAUTH2_USER',
|
||||
MAILER_SMTP_OAUTH2_CLIENT_ID = 'MAILER_SMTP_OAUTH2_CLIENT_ID',
|
||||
MAILER_SMTP_OAUTH2_CLIENT_SECRET = 'MAILER_SMTP_OAUTH2_CLIENT_SECRET',
|
||||
MAILER_SMTP_OAUTH2_REFRESH_TOKEN = 'MAILER_SMTP_OAUTH2_REFRESH_TOKEN',
|
||||
MAILER_SMTP_OAUTH2_ACCESS_URL = 'MAILER_SMTP_OAUTH2_ACCESS_URL',
|
||||
|
||||
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
||||
GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL',
|
||||
|
|
|
|||
|
|
@ -89,6 +89,15 @@
|
|||
"tls_reject_unauthorized": "TLS Reject Unauthorized",
|
||||
"title": "SMTP Configurations",
|
||||
"smtp_auth_incomplete": "SMTP username and password must both be provided or both left empty",
|
||||
"auth_switch_description": "Switching will clear the credentials entered in the current authentication type. Any unsaved data will be lost and cannot be undone.",
|
||||
"auth_type": "Authentication Type",
|
||||
"auth_type_login": "Basic Auth (Login)",
|
||||
"auth_type_oauth2": "OAuth 2.0",
|
||||
"oauth2_user": "OAuth 2.0 User",
|
||||
"oauth2_client_id": "OAuth 2.0 Client ID",
|
||||
"oauth2_client_secret": "OAuth 2.0 Client Secret",
|
||||
"oauth2_refresh_token": "OAuth 2.0 Refresh Token",
|
||||
"oauth2_access_url": "OAuth 2.0 Access Token URL",
|
||||
"toggle_failure": "Failed to toggle smtp!!",
|
||||
"update_failure": "Failed to update smtp configurations!!",
|
||||
"user": "SMTP User"
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@ declare module 'vue' {
|
|||
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||
IconLucideInfo: typeof import('~icons/lucide/info')['default']
|
||||
IconLucideLock: typeof import('~icons/lucide/lock')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideShield: typeof import('~icons/lucide/shield')['default']
|
||||
IconLucideUser: typeof import('~icons/lucide/user')['default']
|
||||
OnboardingAuthProviderCard: typeof import('./components/onboarding/AuthProviderCard.vue')['default']
|
||||
OnboardingAuthSetup: typeof import('./components/onboarding/AuthSetup.vue')['default']
|
||||
|
|
@ -81,5 +83,6 @@ declare module 'vue' {
|
|||
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
|
||||
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default']
|
||||
UsersSuccessInviteModal: typeof import('./components/users/SuccessInviteModal.vue')['default']
|
||||
UsersTeams: typeof import('./components/users/Teams.vue')['default']
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ const props = defineProps<{ isFirstTimeSetup?: boolean }>();
|
|||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'complete-onboarding',
|
||||
payload: { submittingConfigs: boolean; summary: OnBoardingSummary }
|
||||
payload: { submittingConfigs: boolean; summary: OnBoardingSummary },
|
||||
): void;
|
||||
}>();
|
||||
|
||||
|
|
@ -212,10 +212,10 @@ watch(
|
|||
() => enabledConfigs.value,
|
||||
(configs) => {
|
||||
isOAuthEnabled.value = OAuthProviders.some((provider) =>
|
||||
configs.includes(provider)
|
||||
configs.includes(provider),
|
||||
);
|
||||
},
|
||||
{ immediate: true }
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
|
|
@ -229,32 +229,41 @@ watch(
|
|||
selectedOptions.value = ['OAUTH', 'SMTP'];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const isSelected = (option: SelectedOption) =>
|
||||
selectedOptions.value.includes(option as any);
|
||||
|
||||
const isSmtpEnabled = computed(() => enabledConfigs.value.includes('MAILER'));
|
||||
const isSmtpEnabled = computed(
|
||||
() =>
|
||||
enabledConfigs.value.includes('MAILER') ||
|
||||
enabledConfigs.value.includes('EMAIL'),
|
||||
);
|
||||
|
||||
const updateOAuthEnabled = () => {
|
||||
isOAuthEnabled.value = OAuthProviders.some((provider) =>
|
||||
enabledConfigs.value.includes(provider)
|
||||
enabledConfigs.value.includes(provider),
|
||||
);
|
||||
};
|
||||
|
||||
// Card clicks reconcile enabled state to match the card's selection, rather
|
||||
// than blindly flipping. When the backend pre-populated enabledConfigs
|
||||
// (e.g. a prior partial onboarding), a plain toggle would wipe the existing
|
||||
// EMAIL/GOOGLE entries the moment the user clicks the card to "select" them.
|
||||
const toggleSelectedOption = (option: SelectedOption) => {
|
||||
if (selectedOptions.value.includes(option as any)) {
|
||||
selectedOptions.value = selectedOptions.value.filter(
|
||||
(opt) => opt !== option
|
||||
);
|
||||
} else {
|
||||
selectedOptions.value.push(option as any);
|
||||
}
|
||||
const willBeSelected = !selectedOptions.value.includes(option);
|
||||
|
||||
selectedOptions.value = willBeSelected
|
||||
? [...selectedOptions.value, option]
|
||||
: selectedOptions.value.filter((opt) => opt !== option);
|
||||
|
||||
if (option === 'SMTP') {
|
||||
toggleConfig('EMAIL');
|
||||
toggleSmtpConfig();
|
||||
} else {
|
||||
if (willBeSelected !== isSmtpEnabled.value) {
|
||||
toggleConfig('EMAIL');
|
||||
toggleSmtpConfig();
|
||||
}
|
||||
} else if (willBeSelected !== isOAuthEnabled.value) {
|
||||
toggleConfig(option);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
<div class="space-y-2 flex flex-col my-4">
|
||||
<UiAccordion
|
||||
v-for="[provider, value] in Object.entries(currentConfigs.oAuthProviders)"
|
||||
:initial-open="enabledConfigs.includes(provider as EnabledConfig)"
|
||||
:key="provider"
|
||||
:initial-open="shouldOpenProvider(provider as OAuthProvider)"
|
||||
>
|
||||
<template v-slot:header="{ isOpen, toggleAccordion }">
|
||||
<div class="flex items-center justify-between flex-1">
|
||||
|
|
@ -88,6 +89,21 @@ const emit = defineEmits<{
|
|||
|
||||
const currentConfigs = useVModel(props, 'currentConfigs');
|
||||
|
||||
// A provider accordion should auto-expand if it's in enabledConfigs OR if the
|
||||
// backend has returned a saved CLIENT_ID for it. Checking CLIENT_ID is a
|
||||
// direct, reliable signal independent of enabledConfigs propagation timing —
|
||||
// if the backend has data for the provider, it was configured before.
|
||||
const shouldOpenProvider = (provider: OAuthProvider): boolean => {
|
||||
if (props.enabledConfigs.includes(provider)) return true;
|
||||
// Each provider's config object has a `${provider}_CLIENT_ID` key, but
|
||||
// TS cannot narrow the union literal here — cast is intentional.
|
||||
const providerConfig = props.currentConfigs.oAuthProviders[provider] as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
return !!providerConfig[`${provider}_CLIENT_ID`];
|
||||
};
|
||||
|
||||
// check if the key is a callback URL
|
||||
const isCallbackUrl = (key: string): boolean => {
|
||||
return key.toLowerCase().includes('callback');
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
<template>
|
||||
<div class="my-4">
|
||||
<!-- Basic SMTP Fields -->
|
||||
<div class="my-2">
|
||||
<!-- SMTP URL -->
|
||||
<div class="flex flex-col space-y-2">
|
||||
<HoppSmartInput
|
||||
v-model="currentConfigs.mailerConfigs[smtp.ADDRESS_FROM.id]"
|
||||
:label="smtp.ADDRESS_FROM.text"
|
||||
:autofocus="false"
|
||||
input-styles="floating-input"
|
||||
class="!my-2 !bg-primaryLight flex-1"
|
||||
/>
|
||||
<div class="py-2 mb-4">
|
||||
<!-- Custom Configs -->
|
||||
<HoppSmartCheckbox
|
||||
:on="smtp.USE_CUSTOM_CONFIGS.enabled"
|
||||
@change="toggleConfig('USE_CUSTOM_CONFIGS')"
|
||||
>
|
||||
{{ smtp.USE_CUSTOM_CONFIGS.text }}
|
||||
</HoppSmartCheckbox>
|
||||
<p class="text-secondaryLight text-tiny mt-2">
|
||||
{{ t('onboarding.smtp_advanced_config_enable') }}
|
||||
</p>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-if="smtp.SMTP_URL.enabled"
|
||||
v-model="currentConfigs.mailerConfigs[smtp.SMTP_URL.id]"
|
||||
|
|
@ -19,19 +24,69 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Custom Config Fields -->
|
||||
<!-- Host & Port -->
|
||||
<div v-if="smtp.USE_CUSTOM_CONFIGS.enabled" class="flex flex-col space-y-2">
|
||||
<HoppSmartInput
|
||||
v-for="key in ['SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASSWORD'] as AllMailerConfigKeys[]"
|
||||
v-for="key in connectionKeys"
|
||||
:key="key"
|
||||
v-model="currentConfigs.mailerConfigs[smtp[key].id]"
|
||||
:label="smtp[key].text"
|
||||
input-styles="floating-input"
|
||||
:type="key === 'SMTP_PASSWORD' ? 'password' : 'text'"
|
||||
class="!my-2 !bg-primaryLight flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Custom Config Checkboxes -->
|
||||
<!-- From Address (always visible) -->
|
||||
<HoppSmartInput
|
||||
v-model="currentConfigs.mailerConfigs[smtp.ADDRESS_FROM.id]"
|
||||
:label="smtp.ADDRESS_FROM.text"
|
||||
:autofocus="false"
|
||||
input-styles="floating-input"
|
||||
class="!my-2 !bg-primaryLight flex-1"
|
||||
/>
|
||||
|
||||
<!-- Auth & Security -->
|
||||
<div v-if="smtp.USE_CUSTOM_CONFIGS.enabled" class="flex flex-col space-y-2">
|
||||
<!-- Auth Type Tabs + Auth Credentials -->
|
||||
<HoppSmartTabs v-model="authType" render-inactive-tabs>
|
||||
<HoppSmartTab
|
||||
id="login"
|
||||
:icon="IconLock"
|
||||
:label="t('configs.mail_configs.auth_type_login')"
|
||||
>
|
||||
<div class="flex flex-col space-y-2 pt-4">
|
||||
<HoppSmartInput
|
||||
v-for="key in loginAuthKeys"
|
||||
:key="key"
|
||||
v-model="currentConfigs.mailerConfigs[smtp[key].id]"
|
||||
:label="smtp[key].text"
|
||||
input-styles="floating-input"
|
||||
:type="key === 'SMTP_PASSWORD' ? 'password' : 'text'"
|
||||
class="!my-2 !bg-primaryLight flex-1"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab
|
||||
id="oauth2"
|
||||
:icon="IconShield"
|
||||
:label="t('configs.mail_configs.auth_type_oauth2')"
|
||||
>
|
||||
<div class="flex flex-col space-y-2 pt-4">
|
||||
<HoppSmartInput
|
||||
v-for="key in oauth2Keys"
|
||||
:key="key"
|
||||
v-model="currentConfigs.mailerConfigs[smtp[key].id]"
|
||||
:label="smtp[key].text"
|
||||
input-styles="floating-input"
|
||||
:type="isOAuth2SecretField(key) ? 'password' : 'text'"
|
||||
class="!my-2 !bg-primaryLight flex-1"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
|
||||
<!-- TLS checkboxes -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<HoppSmartCheckbox
|
||||
:on="smtp.SMTP_SECURE.enabled"
|
||||
|
|
@ -56,17 +111,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle: Use Custom Configs -->
|
||||
<HoppSmartCheckbox
|
||||
class="mt-4"
|
||||
:on="smtp.USE_CUSTOM_CONFIGS.enabled"
|
||||
@change="toggleConfig('USE_CUSTOM_CONFIGS')"
|
||||
>
|
||||
{{ smtp.USE_CUSTOM_CONFIGS.text }}
|
||||
</HoppSmartCheckbox>
|
||||
<p class="text-secondaryLight text-tiny mt-2">
|
||||
{{ t('onboarding.smtp_advanced_config_enable') }}
|
||||
</p>
|
||||
<HoppSmartConfirmModal
|
||||
:show="showAuthSwitchModal"
|
||||
:title="t('configs.mail_configs.auth_switch_description')"
|
||||
@hide-modal="cancelAuthSwitch"
|
||||
@resolve="confirmAuthSwitch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -78,7 +128,10 @@ import {
|
|||
EnabledConfig,
|
||||
MailerConfigKeys,
|
||||
} from '~/composables/useOnboardingConfigHandler';
|
||||
import { useSmtpAuthTypeSwitch } from '~/composables/useSmtpAuthTypeSwitch';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import IconLock from '~icons/lucide/lock';
|
||||
import IconShield from '~icons/lucide/shield';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
|
|
@ -124,6 +177,18 @@ const smtp = computed<MailerConfig>(() => {
|
|||
value: cfg.MAILER_USE_CUSTOM_CONFIGS,
|
||||
enabled: isCustom,
|
||||
},
|
||||
SMTP_HOST: {
|
||||
id: 'MAILER_SMTP_HOST',
|
||||
text: t('configs.mail_configs.host'),
|
||||
value: cfg.MAILER_SMTP_HOST,
|
||||
enabled: isCustom,
|
||||
},
|
||||
SMTP_PORT: {
|
||||
id: 'MAILER_SMTP_PORT',
|
||||
text: t('configs.mail_configs.port'),
|
||||
value: cfg.MAILER_SMTP_PORT,
|
||||
enabled: isCustom,
|
||||
},
|
||||
SMTP_SECURE: {
|
||||
id: 'MAILER_SMTP_SECURE',
|
||||
text: t('configs.mail_configs.secure'),
|
||||
|
|
@ -154,25 +219,80 @@ const smtp = computed<MailerConfig>(() => {
|
|||
value: cfg.MAILER_SMTP_PASSWORD,
|
||||
enabled: isCustom,
|
||||
},
|
||||
SMTP_HOST: {
|
||||
id: 'MAILER_SMTP_HOST',
|
||||
text: t('configs.mail_configs.host'),
|
||||
value: cfg.MAILER_SMTP_HOST,
|
||||
SMTP_AUTH_TYPE: {
|
||||
id: 'MAILER_SMTP_AUTH_TYPE',
|
||||
text: t('configs.mail_configs.auth_type'),
|
||||
value: cfg.MAILER_SMTP_AUTH_TYPE,
|
||||
enabled: isCustom,
|
||||
},
|
||||
SMTP_PORT: {
|
||||
id: 'MAILER_SMTP_PORT',
|
||||
text: t('configs.mail_configs.port'),
|
||||
value: cfg.MAILER_SMTP_PORT,
|
||||
enabled: isCustom,
|
||||
SMTP_OAUTH2_USER: {
|
||||
id: 'MAILER_SMTP_OAUTH2_USER',
|
||||
text: t('configs.mail_configs.oauth2_user'),
|
||||
value: cfg.MAILER_SMTP_OAUTH2_USER,
|
||||
enabled: isCustom && cfg.MAILER_SMTP_AUTH_TYPE === 'oauth2',
|
||||
},
|
||||
SMTP_OAUTH2_CLIENT_ID: {
|
||||
id: 'MAILER_SMTP_OAUTH2_CLIENT_ID',
|
||||
text: t('configs.mail_configs.oauth2_client_id'),
|
||||
value: cfg.MAILER_SMTP_OAUTH2_CLIENT_ID,
|
||||
enabled: isCustom && cfg.MAILER_SMTP_AUTH_TYPE === 'oauth2',
|
||||
},
|
||||
SMTP_OAUTH2_CLIENT_SECRET: {
|
||||
id: 'MAILER_SMTP_OAUTH2_CLIENT_SECRET',
|
||||
text: t('configs.mail_configs.oauth2_client_secret'),
|
||||
value: cfg.MAILER_SMTP_OAUTH2_CLIENT_SECRET,
|
||||
enabled: isCustom && cfg.MAILER_SMTP_AUTH_TYPE === 'oauth2',
|
||||
},
|
||||
SMTP_OAUTH2_REFRESH_TOKEN: {
|
||||
id: 'MAILER_SMTP_OAUTH2_REFRESH_TOKEN',
|
||||
text: t('configs.mail_configs.oauth2_refresh_token'),
|
||||
value: cfg.MAILER_SMTP_OAUTH2_REFRESH_TOKEN,
|
||||
enabled: isCustom && cfg.MAILER_SMTP_AUTH_TYPE === 'oauth2',
|
||||
},
|
||||
SMTP_OAUTH2_ACCESS_URL: {
|
||||
id: 'MAILER_SMTP_OAUTH2_ACCESS_URL',
|
||||
text: t('configs.mail_configs.oauth2_access_url'),
|
||||
value: cfg.MAILER_SMTP_OAUTH2_ACCESS_URL,
|
||||
enabled: isCustom && cfg.MAILER_SMTP_AUTH_TYPE === 'oauth2',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Field key groups in logical UX order
|
||||
const connectionKeys: AllMailerConfigKeys[] = ['SMTP_HOST', 'SMTP_PORT'];
|
||||
const loginAuthKeys: AllMailerConfigKeys[] = ['SMTP_USER', 'SMTP_PASSWORD'];
|
||||
const oauth2Keys: AllMailerConfigKeys[] = [
|
||||
'SMTP_OAUTH2_USER',
|
||||
'SMTP_OAUTH2_CLIENT_ID',
|
||||
'SMTP_OAUTH2_CLIENT_SECRET',
|
||||
'SMTP_OAUTH2_REFRESH_TOKEN',
|
||||
'SMTP_OAUTH2_ACCESS_URL',
|
||||
];
|
||||
|
||||
const isOAuth2SecretField = (key: AllMailerConfigKeys) =>
|
||||
['SMTP_OAUTH2_CLIENT_SECRET', 'SMTP_OAUTH2_REFRESH_TOKEN'].includes(key);
|
||||
|
||||
const toggleConfig = (key: AllMailerConfigKeys) => {
|
||||
const id = smtp.value[key].id;
|
||||
const current = currentConfigs.value.mailerConfigs[id];
|
||||
currentConfigs.value.mailerConfigs[id] =
|
||||
current === 'true' ? 'false' : 'true';
|
||||
};
|
||||
|
||||
const LOGIN_KEYS: ConfigKey[] = ['MAILER_SMTP_USER', 'MAILER_SMTP_PASSWORD'];
|
||||
const OAUTH2_KEYS: ConfigKey[] = [
|
||||
'MAILER_SMTP_OAUTH2_USER',
|
||||
'MAILER_SMTP_OAUTH2_CLIENT_ID',
|
||||
'MAILER_SMTP_OAUTH2_CLIENT_SECRET',
|
||||
'MAILER_SMTP_OAUTH2_REFRESH_TOKEN',
|
||||
'MAILER_SMTP_OAUTH2_ACCESS_URL',
|
||||
];
|
||||
|
||||
const { authType, showAuthSwitchModal, confirmAuthSwitch, cancelAuthSwitch } =
|
||||
useSmtpAuthTypeSwitch<ConfigKey>(
|
||||
() => currentConfigs.value.mailerConfigs,
|
||||
'MAILER_SMTP_AUTH_TYPE',
|
||||
LOGIN_KEYS,
|
||||
OAUTH2_KEYS,
|
||||
);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -46,59 +46,191 @@
|
|||
{{ t('configs.mail_configs.custom_smtp_configs') }}
|
||||
</HoppSmartCheckbox>
|
||||
</div>
|
||||
<div
|
||||
v-for="field in smtpConfigFields"
|
||||
:key="field.key"
|
||||
class="mt-5 ml-12"
|
||||
>
|
||||
<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)"
|
||||
>
|
||||
{{ makeReadableKey(field.name, true) }}
|
||||
</HoppSmartCheckbox>
|
||||
</div>
|
||||
<span v-else>
|
||||
<label>{{ makeReadableKey(field.name, true) }}</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 border border-divider rounded"
|
||||
input-styles="!border-0"
|
||||
>
|
||||
<template #button>
|
||||
<HoppButtonSecondary
|
||||
:icon="isMasked(field.key) ? IconEye : IconEyeOff"
|
||||
class="bg-primaryLight rounded"
|
||||
@click="toggleMask(field.key)"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
</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"
|
||||
<!-- SMTP URL -->
|
||||
<template v-if="!smtpConfigs.fields.mailer_use_custom_configs">
|
||||
<div
|
||||
v-for="field in basicFields"
|
||||
:key="field.key"
|
||||
class="mt-5 ml-12"
|
||||
>
|
||||
<label>{{ field.name }}</label>
|
||||
<span class="flex max-w-lg">
|
||||
<HoppSmartInput
|
||||
v-model="stringFields[field.key]"
|
||||
:type="isMasked(field.key) ? 'password' : 'text'"
|
||||
:autofocus="false"
|
||||
class="!my-2 !bg-primaryLight flex-1 border border-divider rounded"
|
||||
input-styles="!border-0"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<icon-lucide-info class="mr-2" />
|
||||
<span> {{ field.error }} </span>
|
||||
</div>
|
||||
<template #button>
|
||||
<HoppButtonSecondary
|
||||
:icon="isMasked(field.key) ? IconEye : IconEyeOff"
|
||||
class="bg-primaryLight rounded"
|
||||
@click="toggleMask(field.key)"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- CUSTOM MODE -->
|
||||
<template v-if="smtpConfigs.fields.mailer_use_custom_configs">
|
||||
<!-- Host & Port -->
|
||||
<div
|
||||
v-for="field in connectionFields"
|
||||
:key="field.key"
|
||||
class="mt-5 ml-12"
|
||||
>
|
||||
<label>{{ field.name }}</label>
|
||||
<span class="flex max-w-lg">
|
||||
<HoppSmartInput
|
||||
v-model="stringFields[field.key]"
|
||||
:type="isMasked(field.key) ? 'password' : 'text'"
|
||||
:autofocus="false"
|
||||
class="!my-2 !bg-primaryLight flex-1 border border-divider rounded"
|
||||
input-styles="!border-0"
|
||||
>
|
||||
<template #button>
|
||||
<HoppButtonSecondary
|
||||
:icon="isMasked(field.key) ? IconEye : IconEyeOff"
|
||||
class="bg-primaryLight rounded"
|
||||
@click="toggleMask(field.key)"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- From Address (always visible) -->
|
||||
<div class="mt-5 ml-12">
|
||||
<label>{{ t('configs.mail_configs.address_from') }}</label>
|
||||
<span class="flex max-w-lg">
|
||||
<HoppSmartInput
|
||||
v-model="smtpConfigs.fields.mailer_from_address"
|
||||
:type="isMasked('mailer_from_address') ? 'password' : 'text'"
|
||||
:autofocus="false"
|
||||
class="!my-2 !bg-primaryLight flex-1 border border-divider rounded"
|
||||
input-styles="!border-0"
|
||||
>
|
||||
<template #button>
|
||||
<HoppButtonSecondary
|
||||
:icon="
|
||||
isMasked('mailer_from_address') ? IconEye : IconEyeOff
|
||||
"
|
||||
class="bg-primaryLight rounded"
|
||||
@click="toggleMask('mailer_from_address')"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Custom mode: Auth & Security -->
|
||||
<template v-if="smtpConfigs.fields.mailer_use_custom_configs">
|
||||
<!-- Auth Type Tabs + Auth Credentials -->
|
||||
<div class="mt-5 ml-12 max-w-lg">
|
||||
<HoppSmartTabs v-model="authType" render-inactive-tabs>
|
||||
<HoppSmartTab
|
||||
id="login"
|
||||
:icon="IconLock"
|
||||
:label="t('configs.mail_configs.auth_type_login')"
|
||||
>
|
||||
<div class="space-y-1 pt-4">
|
||||
<div v-for="field in loginAuthFields" :key="field.key">
|
||||
<label>{{ field.name }}</label>
|
||||
<span class="flex">
|
||||
<HoppSmartInput
|
||||
v-model="stringFields[field.key]"
|
||||
:type="isMasked(field.key) ? 'password' : 'text'"
|
||||
:autofocus="false"
|
||||
class="!my-2 !bg-primaryLight flex-1 border border-divider rounded"
|
||||
input-styles="!border-0"
|
||||
>
|
||||
<template #button>
|
||||
<HoppButtonSecondary
|
||||
:icon="
|
||||
isMasked(field.key) ? IconEye : IconEyeOff
|
||||
"
|
||||
class="bg-primaryLight rounded"
|
||||
@click="toggleMask(field.key)"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab
|
||||
id="oauth2"
|
||||
:icon="IconShield"
|
||||
:label="t('configs.mail_configs.auth_type_oauth2')"
|
||||
>
|
||||
<div class="space-y-1 pt-4">
|
||||
<div v-for="field in oauth2Fields" :key="field.key">
|
||||
<label>{{ field.name }}</label>
|
||||
<span class="flex">
|
||||
<HoppSmartInput
|
||||
v-model="stringFields[field.key]"
|
||||
:type="isMasked(field.key) ? 'password' : 'text'"
|
||||
:autofocus="false"
|
||||
class="!my-2 !bg-primaryLight flex-1 border border-divider rounded"
|
||||
input-styles="!border-0"
|
||||
>
|
||||
<template #button>
|
||||
<HoppButtonSecondary
|
||||
:icon="
|
||||
isMasked(field.key) ? IconEye : IconEyeOff
|
||||
"
|
||||
class="bg-primaryLight rounded"
|
||||
@click="toggleMask(field.key)"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</div>
|
||||
|
||||
<!-- TLS checkboxes -->
|
||||
<div
|
||||
v-for="field in securityFields"
|
||||
:key="field.key"
|
||||
class="mt-5 ml-12 flex flex-col items-start gap-5"
|
||||
>
|
||||
<HoppSmartCheckbox
|
||||
:on="Boolean(smtpConfigs.fields[field.key])"
|
||||
@change="toggleCheckbox(field)"
|
||||
>
|
||||
{{ field.name }}
|
||||
</HoppSmartCheckbox>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="showAuthSwitchModal"
|
||||
:title="t('configs.mail_configs.auth_switch_description')"
|
||||
@hide-modal="cancelAuthSwitch"
|
||||
@resolve="confirmAuthSwitch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -106,11 +238,13 @@
|
|||
import { useVModel } from '@vueuse/core';
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useSmtpAuthTypeSwitch } from '~/composables/useSmtpAuthTypeSwitch';
|
||||
import { hasInputValidationFailed, ServerConfigs } from '~/helpers/configs';
|
||||
import { makeReadableKey } from '~/helpers/utils/readableKey';
|
||||
import IconEye from '~icons/lucide/eye';
|
||||
import IconEyeOff from '~icons/lucide/eye-off';
|
||||
import IconHelpCircle from '~icons/lucide/help-circle';
|
||||
import IconLock from '~icons/lucide/lock';
|
||||
import IconShield from '~icons/lucide/shield';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
|
|
@ -124,7 +258,6 @@ const emit = defineEmits<{
|
|||
|
||||
const workingConfigs = useVModel(props, 'config', emit);
|
||||
|
||||
// Get or set smtpConfigs from workingConfigs
|
||||
const smtpConfigs = computed({
|
||||
get() {
|
||||
return workingConfigs.value?.mailConfigs;
|
||||
|
|
@ -134,43 +267,82 @@ const smtpConfigs = computed({
|
|||
},
|
||||
});
|
||||
|
||||
// Mask sensitive fields
|
||||
type MailFields = ServerConfigs['mailConfigs']['fields'];
|
||||
|
||||
// Extract only the keys whose value type is `string`
|
||||
type StringFieldKey = {
|
||||
[K in keyof MailFields]: MailFields[K] extends string ? K : never;
|
||||
}[keyof MailFields];
|
||||
|
||||
type BooleanFieldKey = {
|
||||
[K in keyof MailFields]: MailFields[K] extends boolean ? K : never;
|
||||
}[keyof MailFields];
|
||||
|
||||
type Field = {
|
||||
name: string;
|
||||
key: keyof ServerConfigs['mailConfigs']['fields'];
|
||||
key: StringFieldKey;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const smtpConfigFields = reactive<Field[]>([
|
||||
type CheckboxField = {
|
||||
name: string;
|
||||
key: BooleanFieldKey;
|
||||
};
|
||||
|
||||
// A typed view of `fields` that only exposes string-valued keys,
|
||||
// so that `v-model="stringFields[field.key]"` resolves to `string`.
|
||||
const stringFields = computed(
|
||||
() => smtpConfigs.value.fields as Pick<MailFields, StringFieldKey>,
|
||||
);
|
||||
|
||||
// Basic mode: just the SMTP URL
|
||||
const basicFields: Field[] = [
|
||||
{
|
||||
name: t('configs.mail_configs.smtp_url'),
|
||||
key: 'mailer_smtp_url',
|
||||
error: t('configs.mail_configs.input_validation'),
|
||||
},
|
||||
];
|
||||
|
||||
// Connection
|
||||
const connectionFields: Field[] = [
|
||||
{ name: t('configs.mail_configs.host'), key: 'mailer_smtp_host' },
|
||||
{ name: t('configs.mail_configs.port'), key: 'mailer_smtp_port' },
|
||||
];
|
||||
|
||||
// Login auth credentials
|
||||
const loginAuthFields: Field[] = [
|
||||
{ name: t('configs.mail_configs.user'), key: 'mailer_smtp_user' },
|
||||
{ name: t('configs.mail_configs.password'), key: 'mailer_smtp_password' },
|
||||
];
|
||||
|
||||
// OAuth2 auth credentials
|
||||
const oauth2Fields: Field[] = [
|
||||
{
|
||||
name: t('configs.mail_configs.address_from'),
|
||||
key: 'mailer_from_address',
|
||||
name: t('configs.mail_configs.oauth2_user'),
|
||||
key: 'mailer_smtp_oauth2_user',
|
||||
},
|
||||
{
|
||||
name: t('configs.mail_configs.host'),
|
||||
key: 'mailer_smtp_host',
|
||||
name: t('configs.mail_configs.oauth2_client_id'),
|
||||
key: 'mailer_smtp_oauth2_client_id',
|
||||
},
|
||||
{
|
||||
name: t('configs.mail_configs.port'),
|
||||
key: 'mailer_smtp_port',
|
||||
name: t('configs.mail_configs.oauth2_client_secret'),
|
||||
key: 'mailer_smtp_oauth2_client_secret',
|
||||
},
|
||||
{
|
||||
name: t('configs.mail_configs.user'),
|
||||
key: 'mailer_smtp_user',
|
||||
name: t('configs.mail_configs.oauth2_refresh_token'),
|
||||
key: 'mailer_smtp_oauth2_refresh_token',
|
||||
},
|
||||
{
|
||||
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.oauth2_access_url'),
|
||||
key: 'mailer_smtp_oauth2_access_url',
|
||||
},
|
||||
];
|
||||
|
||||
// Security checkboxes
|
||||
const securityFields: CheckboxField[] = [
|
||||
{ name: t('configs.mail_configs.secure'), key: 'mailer_smtp_secure' },
|
||||
{
|
||||
name: t('configs.mail_configs.ignore_tls'),
|
||||
key: 'mailer_smtp_ignore_tls',
|
||||
|
|
@ -179,7 +351,7 @@ const smtpConfigFields = reactive<Field[]>([
|
|||
name: t('configs.mail_configs.tls_reject_unauthorized'),
|
||||
key: 'mailer_tls_reject_unauthorized',
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
const maskState = reactive<Record<string, boolean>>({
|
||||
mailer_smtp_url: true,
|
||||
|
|
@ -188,6 +360,11 @@ const maskState = reactive<Record<string, boolean>>({
|
|||
mailer_smtp_port: true,
|
||||
mailer_smtp_user: true,
|
||||
mailer_smtp_password: true,
|
||||
mailer_smtp_oauth2_user: true,
|
||||
mailer_smtp_oauth2_client_id: true,
|
||||
mailer_smtp_oauth2_client_secret: true,
|
||||
mailer_smtp_oauth2_refresh_token: true,
|
||||
mailer_smtp_oauth2_access_url: true,
|
||||
});
|
||||
|
||||
const toggleMask = (fieldKey: keyof ServerConfigs['mailConfigs']['fields']) => {
|
||||
|
|
@ -197,41 +374,8 @@ 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_smtp_ignore_tls',
|
||||
'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_smtp_ignore_tls',
|
||||
'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]);
|
||||
const toggleCheckbox = (field: CheckboxField) =>
|
||||
(smtpConfigs.value.fields[field.key] = !smtpConfigs.value.fields[field.key]);
|
||||
|
||||
// Input Validation
|
||||
const fieldErrors = computed(() => {
|
||||
|
|
@ -246,11 +390,29 @@ const fieldErrors = computed(() => {
|
|||
return errors;
|
||||
});
|
||||
|
||||
const getFieldError = (
|
||||
fieldKey: keyof ServerConfigs['mailConfigs']['fields']
|
||||
) => fieldErrors.value[fieldKey];
|
||||
const getFieldError = (fieldKey: StringFieldKey) => fieldErrors.value[fieldKey];
|
||||
|
||||
watch(fieldErrors, (errors) => {
|
||||
hasInputValidationFailed.value = Object.values(errors).some(Boolean);
|
||||
});
|
||||
|
||||
const LOGIN_KEYS: StringFieldKey[] = [
|
||||
'mailer_smtp_user',
|
||||
'mailer_smtp_password',
|
||||
];
|
||||
const OAUTH2_KEYS: StringFieldKey[] = [
|
||||
'mailer_smtp_oauth2_user',
|
||||
'mailer_smtp_oauth2_client_id',
|
||||
'mailer_smtp_oauth2_client_secret',
|
||||
'mailer_smtp_oauth2_refresh_token',
|
||||
'mailer_smtp_oauth2_access_url',
|
||||
];
|
||||
|
||||
const { authType, showAuthSwitchModal, confirmAuthSwitch, cancelAuthSwitch } =
|
||||
useSmtpAuthTypeSwitch<StringFieldKey>(
|
||||
() => stringFields.value,
|
||||
'mailer_smtp_auth_type',
|
||||
LOGIN_KEYS,
|
||||
OAUTH2_KEYS,
|
||||
);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
|
||||
const t = useI18n();
|
||||
|
|
@ -60,18 +60,18 @@ const idSuffix = Math.random().toString(36).substring(2, 9);
|
|||
const contentId = `accordion-content-${idSuffix}`;
|
||||
const headerId = `accordion-header-${idSuffix}`;
|
||||
|
||||
const isOpen = ref(false);
|
||||
// `isOpen` follows `initialOpen` reactively until the user toggles manually.
|
||||
// After a manual toggle, `userOverride` takes precedence so subsequent
|
||||
// prop changes (e.g. backend data arriving late) don't snap the accordion
|
||||
// back open/closed against the user's intent.
|
||||
const userOverride = ref<boolean | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.initialOpen,
|
||||
(val) => {
|
||||
isOpen.value = val ?? false;
|
||||
},
|
||||
{ immediate: true }
|
||||
const isOpen = computed(() =>
|
||||
userOverride.value !== null ? userOverride.value : props.initialOpen ?? false
|
||||
);
|
||||
|
||||
const toggleAccordion = () => {
|
||||
isOpen.value = !isOpen.value;
|
||||
emit('toggle', isOpen.value);
|
||||
userOverride.value = !isOpen.value;
|
||||
emit('toggle', userOverride.value);
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -144,6 +144,23 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
|||
'true',
|
||||
mailer_use_custom_configs:
|
||||
getFieldValue(InfraConfigEnum.MailerUseCustomConfigs) === 'true',
|
||||
mailer_smtp_auth_type:
|
||||
getFieldValue(InfraConfigEnum.MailerSmtpAuthType) || 'login',
|
||||
mailer_smtp_oauth2_user: getFieldValue(
|
||||
InfraConfigEnum.MailerSmtpOauth2User
|
||||
),
|
||||
mailer_smtp_oauth2_client_id: getFieldValue(
|
||||
InfraConfigEnum.MailerSmtpOauth2ClientId
|
||||
),
|
||||
mailer_smtp_oauth2_client_secret: getFieldValue(
|
||||
InfraConfigEnum.MailerSmtpOauth2ClientSecret
|
||||
),
|
||||
mailer_smtp_oauth2_refresh_token: getFieldValue(
|
||||
InfraConfigEnum.MailerSmtpOauth2RefreshToken
|
||||
),
|
||||
mailer_smtp_oauth2_access_url: getFieldValue(
|
||||
InfraConfigEnum.MailerSmtpOauth2AccessUrl
|
||||
),
|
||||
},
|
||||
},
|
||||
tokenConfigs: {
|
||||
|
|
@ -277,13 +294,23 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
|||
|
||||
// SMTP user and password are optional as a pair (both or neither)
|
||||
const optionalMailerKeys = ['mailer_smtp_user', 'mailer_smtp_password'];
|
||||
// OAuth2 fields are always optional and auth_type has a default
|
||||
const oauth2Keys = [
|
||||
'mailer_smtp_auth_type',
|
||||
'mailer_smtp_oauth2_user',
|
||||
'mailer_smtp_oauth2_client_id',
|
||||
'mailer_smtp_oauth2_client_secret',
|
||||
'mailer_smtp_oauth2_refresh_token',
|
||||
'mailer_smtp_oauth2_access_url',
|
||||
];
|
||||
const excludeKeys = mailer_use_custom_configs
|
||||
? ['mailer_smtp_url', ...optionalMailerKeys]
|
||||
? ['mailer_smtp_url', ...optionalMailerKeys, ...oauth2Keys]
|
||||
: [
|
||||
'mailer_smtp_host',
|
||||
'mailer_smtp_port',
|
||||
'mailer_smtp_user',
|
||||
'mailer_smtp_password',
|
||||
...oauth2Keys,
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -323,6 +350,10 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
|||
const fields = config.mailConfigs.fields;
|
||||
if (!fields.mailer_use_custom_configs) return false;
|
||||
|
||||
// Enforced regardless of auth_type: the backend validates the pair
|
||||
// on every save, so stale login values left behind after switching
|
||||
// to the OAuth2 tab would still be rejected. Surface this in the FE
|
||||
// toast so users know to clear those fields before saving.
|
||||
const hasUser = fields.mailer_smtp_user.trim() !== '';
|
||||
const hasPass = fields.mailer_smtp_password.trim() !== '';
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,13 @@ export type MailerConfigKeys =
|
|||
| 'SMTP_USER'
|
||||
| 'SMTP_PASSWORD'
|
||||
| 'SMTP_IGNORE_TLS'
|
||||
| 'TLS_REJECT_UNAUTHORIZED';
|
||||
| 'TLS_REJECT_UNAUTHORIZED'
|
||||
| 'SMTP_AUTH_TYPE'
|
||||
| 'SMTP_OAUTH2_USER'
|
||||
| 'SMTP_OAUTH2_CLIENT_ID'
|
||||
| 'SMTP_OAUTH2_CLIENT_SECRET'
|
||||
| 'SMTP_OAUTH2_REFRESH_TOKEN'
|
||||
| 'SMTP_OAUTH2_ACCESS_URL';
|
||||
|
||||
export type Configs = {
|
||||
oAuthProviders: {
|
||||
|
|
@ -52,7 +58,7 @@ export type OnBoardingSummary = {
|
|||
};
|
||||
|
||||
function mapOAuthProviders(
|
||||
configs: Partial<Record<InfraConfigEnum, string>>
|
||||
configs: Partial<Record<InfraConfigEnum, string>>,
|
||||
): Configs['oAuthProviders'] {
|
||||
return {
|
||||
GOOGLE: {
|
||||
|
|
@ -78,7 +84,7 @@ function mapOAuthProviders(
|
|||
}
|
||||
|
||||
function mapMailerConfigs(
|
||||
configs: Partial<Record<InfraConfigEnum, string>>
|
||||
configs: Partial<Record<InfraConfigEnum, string>>,
|
||||
): Configs['mailerConfigs'] {
|
||||
return {
|
||||
MAILER_SMTP_ENABLE: configs.MAILER_SMTP_ENABLE ?? '',
|
||||
|
|
@ -93,6 +99,14 @@ function mapMailerConfigs(
|
|||
MAILER_SMTP_IGNORE_TLS: configs.MAILER_SMTP_IGNORE_TLS || 'false',
|
||||
MAILER_TLS_REJECT_UNAUTHORIZED:
|
||||
configs.MAILER_TLS_REJECT_UNAUTHORIZED || 'false',
|
||||
MAILER_SMTP_AUTH_TYPE: configs.MAILER_SMTP_AUTH_TYPE || 'login',
|
||||
MAILER_SMTP_OAUTH2_USER: configs.MAILER_SMTP_OAUTH2_USER ?? '',
|
||||
MAILER_SMTP_OAUTH2_CLIENT_ID: configs.MAILER_SMTP_OAUTH2_CLIENT_ID ?? '',
|
||||
MAILER_SMTP_OAUTH2_CLIENT_SECRET:
|
||||
configs.MAILER_SMTP_OAUTH2_CLIENT_SECRET ?? '',
|
||||
MAILER_SMTP_OAUTH2_REFRESH_TOKEN:
|
||||
configs.MAILER_SMTP_OAUTH2_REFRESH_TOKEN ?? '',
|
||||
MAILER_SMTP_OAUTH2_ACCESS_URL: configs.MAILER_SMTP_OAUTH2_ACCESS_URL ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +146,7 @@ export function useOnboardingConfigHandler() {
|
|||
const toggleConfig = (key: EnabledConfig | 'OAUTH' | 'EMAIL') => {
|
||||
if (key === 'OAUTH') {
|
||||
enabledConfigs.value = enabledConfigs.value.filter(
|
||||
(c) => !['GOOGLE', 'GITHUB', 'MICROSOFT'].includes(c)
|
||||
(c) => !['GOOGLE', 'GITHUB', 'MICROSOFT'].includes(c),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +154,7 @@ export function useOnboardingConfigHandler() {
|
|||
const hasEmail = enabledConfigs.value.includes('EMAIL');
|
||||
const hasMailer = enabledConfigs.value.includes('MAILER');
|
||||
enabledConfigs.value = enabledConfigs.value.filter(
|
||||
(c) => c !== 'EMAIL' && c !== 'MAILER'
|
||||
(c) => c !== 'EMAIL' && c !== 'MAILER',
|
||||
);
|
||||
if (!hasEmail || !hasMailer) {
|
||||
enabledConfigs.value.push('EMAIL', 'MAILER');
|
||||
|
|
@ -234,6 +248,12 @@ export function useOnboardingConfigHandler() {
|
|||
'MAILER_SMTP_IGNORE_TLS',
|
||||
'MAILER_TLS_REJECT_UNAUTHORIZED',
|
||||
'MAILER_SMTP_ENABLE',
|
||||
'MAILER_SMTP_AUTH_TYPE',
|
||||
'MAILER_SMTP_OAUTH2_USER',
|
||||
'MAILER_SMTP_OAUTH2_CLIENT_ID',
|
||||
'MAILER_SMTP_OAUTH2_CLIENT_SECRET',
|
||||
'MAILER_SMTP_OAUTH2_REFRESH_TOKEN',
|
||||
'MAILER_SMTP_OAUTH2_ACCESS_URL',
|
||||
].includes(key);
|
||||
});
|
||||
};
|
||||
|
|
@ -252,7 +272,7 @@ export function useOnboardingConfigHandler() {
|
|||
}
|
||||
|
||||
const relevantKeys = Object.keys(configs).filter((key) =>
|
||||
enabledConfigs.value.includes(key.split('_')[0] as EnabledConfig)
|
||||
enabledConfigs.value.includes(key.split('_')[0] as EnabledConfig),
|
||||
);
|
||||
|
||||
const neededKeys = filterNeededConfigs(relevantKeys);
|
||||
|
|
@ -263,9 +283,14 @@ export function useOnboardingConfigHandler() {
|
|||
const optionalSmtpKeys = new Set([
|
||||
'MAILER_SMTP_USER',
|
||||
'MAILER_SMTP_PASSWORD',
|
||||
'MAILER_SMTP_OAUTH2_USER',
|
||||
'MAILER_SMTP_OAUTH2_CLIENT_ID',
|
||||
'MAILER_SMTP_OAUTH2_CLIENT_SECRET',
|
||||
'MAILER_SMTP_OAUTH2_REFRESH_TOKEN',
|
||||
'MAILER_SMTP_OAUTH2_ACCESS_URL',
|
||||
]);
|
||||
const allFilled = neededKeys.every(
|
||||
(key) => configs[key] || optionalSmtpKeys.has(key)
|
||||
(key) => configs[key] || optionalSmtpKeys.has(key),
|
||||
);
|
||||
|
||||
if (!allFilled) {
|
||||
|
|
@ -274,14 +299,17 @@ export function useOnboardingConfigHandler() {
|
|||
toast.error(
|
||||
t('onboarding.please_fill_configurations', {
|
||||
fieldName: makeReadableKey(key),
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// SMTP credentials must be provided together or both left empty.
|
||||
// Only enforce when custom SMTP mode is active (not simple URL mode).
|
||||
// Enforced regardless of auth_type: the backend validates the pair on
|
||||
// every save, so stale login values left behind after switching to the
|
||||
// OAuth2 tab would still be rejected. Surface this in the FE toast so
|
||||
// users know to clear those fields before saving.
|
||||
if (
|
||||
enabledConfigs.value.includes('MAILER') &&
|
||||
configs['MAILER_USE_CUSTOM_CONFIGS'] === 'true'
|
||||
|
|
@ -301,8 +329,8 @@ export function useOnboardingConfigHandler() {
|
|||
Object.entries(configs).filter(
|
||||
([key, val]) =>
|
||||
enabledConfigs.value.includes(key.split('_')[0] as EnabledConfig) &&
|
||||
(val || optionalSmtpKeys.has(key))
|
||||
)
|
||||
(val || optionalSmtpKeys.has(key)),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -331,7 +359,7 @@ export function useOnboardingConfigHandler() {
|
|||
}
|
||||
|
||||
const filteredEnabledConfigs = enabledConfigs.value.filter(
|
||||
(config) => config !== 'OAUTH' && config !== 'MAILER'
|
||||
(config) => config !== 'OAUTH' && config !== 'MAILER',
|
||||
);
|
||||
|
||||
const configWithAuth = {
|
||||
|
|
@ -371,7 +399,22 @@ export function useOnboardingConfigHandler() {
|
|||
|
||||
const allowed = configs[InfraConfigEnum.ViteAllowedAuthProviders];
|
||||
if (allowed) {
|
||||
enabledConfigs.value = allowed.split(',') as EnabledConfig[];
|
||||
// Trim each entry so whitespace variations ("GOOGLE, EMAIL") don't
|
||||
// cause provider-name mismatches in downstream `.includes()` checks.
|
||||
const parsed = allowed
|
||||
.split(',')
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean) as EnabledConfig[];
|
||||
|
||||
// The backend persists only 'EMAIL' in VITE_ALLOWED_AUTH_PROVIDERS,
|
||||
// but internally we also track 'MAILER' as the signal that MAILER_*
|
||||
// keys should be kept on save. toggleConfig('EMAIL') pairs them, so
|
||||
// mirror that invariant on load to keep the two flags in sync.
|
||||
if (parsed.includes('EMAIL') && !parsed.includes('MAILER')) {
|
||||
parsed.push('MAILER');
|
||||
}
|
||||
|
||||
enabledConfigs.value = parsed;
|
||||
}
|
||||
|
||||
currentConfigs.value = {
|
||||
|
|
@ -404,7 +447,7 @@ export function useOnboardingConfigHandler() {
|
|||
enableConfig('EMAIL');
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import { computed, ref } from 'vue';
|
||||
|
||||
// Shared auth-type tab switching logic for SMTP configuration UIs.
|
||||
// When switching between login/oauth2 tabs, if the current tab has data
|
||||
// we confirm before clearing — so stale credentials don't get persisted
|
||||
// alongside the active set.
|
||||
//
|
||||
// `fields` is a lazy accessor to the reactive record (the two call sites
|
||||
// nest it differently: `currentConfigs.mailerConfigs` vs `smtpConfigs.fields`).
|
||||
// Calling it inside computed getters/setters preserves reactivity.
|
||||
export function useSmtpAuthTypeSwitch<K extends string>(
|
||||
fields: () => Record<K, string>,
|
||||
authKey: K,
|
||||
loginKeys: readonly K[],
|
||||
oauth2Keys: readonly K[],
|
||||
) {
|
||||
const showAuthSwitchModal = ref(false);
|
||||
const pendingAuthType = ref<string | null>(null);
|
||||
|
||||
const hasAnyValue = (keys: readonly K[]) =>
|
||||
keys.some((k) => (fields()[k] ?? '').trim() !== '');
|
||||
|
||||
const authType = computed({
|
||||
get: () => fields()[authKey],
|
||||
set: (next: string) => {
|
||||
const current = fields()[authKey];
|
||||
if (next === current) return;
|
||||
|
||||
const keysToClear = current === 'oauth2' ? oauth2Keys : loginKeys;
|
||||
if (hasAnyValue(keysToClear)) {
|
||||
pendingAuthType.value = next;
|
||||
showAuthSwitchModal.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
fields()[authKey] = next;
|
||||
},
|
||||
});
|
||||
|
||||
const confirmAuthSwitch = () => {
|
||||
if (pendingAuthType.value === null) return;
|
||||
const current = fields()[authKey];
|
||||
const keysToClear = current === 'oauth2' ? oauth2Keys : loginKeys;
|
||||
keysToClear.forEach((k) => {
|
||||
fields()[k] = '';
|
||||
});
|
||||
fields()[authKey] = pendingAuthType.value;
|
||||
pendingAuthType.value = null;
|
||||
showAuthSwitchModal.value = false;
|
||||
};
|
||||
|
||||
const cancelAuthSwitch = () => {
|
||||
pendingAuthType.value = null;
|
||||
showAuthSwitchModal.value = false;
|
||||
};
|
||||
|
||||
return {
|
||||
authType,
|
||||
showAuthSwitchModal,
|
||||
confirmAuthSwitch,
|
||||
cancelAuthSwitch,
|
||||
};
|
||||
}
|
||||
|
|
@ -56,6 +56,12 @@ export type ServerConfigs = {
|
|||
mailer_smtp_ignore_tls: boolean;
|
||||
mailer_tls_reject_unauthorized: boolean;
|
||||
mailer_use_custom_configs: boolean;
|
||||
mailer_smtp_auth_type: string;
|
||||
mailer_smtp_oauth2_user: string;
|
||||
mailer_smtp_oauth2_client_id: string;
|
||||
mailer_smtp_oauth2_client_secret: string;
|
||||
mailer_smtp_oauth2_refresh_token: string;
|
||||
mailer_smtp_oauth2_access_url: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -230,6 +236,30 @@ export const CUSTOM_MAIL_CONFIGS: Config[] = [
|
|||
name: InfraConfigEnum.MailerTlsRejectUnauthorized,
|
||||
key: 'mailer_tls_reject_unauthorized',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerSmtpAuthType,
|
||||
key: 'mailer_smtp_auth_type',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerSmtpOauth2User,
|
||||
key: 'mailer_smtp_oauth2_user',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerSmtpOauth2ClientId,
|
||||
key: 'mailer_smtp_oauth2_client_id',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerSmtpOauth2ClientSecret,
|
||||
key: 'mailer_smtp_oauth2_client_secret',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerSmtpOauth2RefreshToken,
|
||||
key: 'mailer_smtp_oauth2_refresh_token',
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MailerSmtpOauth2AccessUrl,
|
||||
key: 'mailer_smtp_oauth2_access_url',
|
||||
},
|
||||
];
|
||||
|
||||
const DATA_SHARING_CONFIGS: Omit<Config, 'key'>[] = [
|
||||
|
|
|
|||
Loading…
Reference in a new issue