mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-10 08:05:12 +00:00
feat: SMTP xoauth2 in console (#11545)
# Which Problems Are Solved This adds the newly added xoauth2 option to the console allowing SMTP Providers to be setup that use xoauth2 instead of plain auth. # How the Problems Are Solved Added the xoauth2 option to the SMTP Provider Settings # Additional Changes Heavily refactored the SMTP Provider page to allow more dynamic configuration and use Tanstack Query for Data Fetching. # Additional Context - Closes #11451 - Backend changes #11239
This commit is contained in:
@@ -1,41 +1,24 @@
|
||||
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { Subject, take } from 'rxjs';
|
||||
import { StepperSelectionEvent } from '@angular/cdk/stepper';
|
||||
import { Options } from 'src/app/proto/generated/zitadel/idp_pb';
|
||||
import { Component, computed, effect, inject, linkedSignal, Signal, viewChild } from '@angular/core';
|
||||
import { FormBuilder, FormControl, Validators } from '@angular/forms';
|
||||
import { requiredValidator } from '../form-field/validators/validators';
|
||||
|
||||
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
|
||||
import {
|
||||
AddSMTPConfigRequest,
|
||||
AddSMTPConfigResponse,
|
||||
TestSMTPConfigRequest,
|
||||
UpdateSMTPConfigRequest,
|
||||
UpdateSMTPConfigResponse,
|
||||
} from 'src/app/proto/generated/zitadel/admin_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import {
|
||||
AmazonSESDefaultSettings,
|
||||
BrevoDefaultSettings,
|
||||
GenericDefaultSettings,
|
||||
GoogleDefaultSettings,
|
||||
MailchimpDefaultSettings,
|
||||
MailgunDefaultSettings,
|
||||
MailjetDefaultSettings,
|
||||
PostmarkDefaultSettings,
|
||||
ProviderDefaultSettings,
|
||||
OutlookDefaultSettings,
|
||||
SendgridDefaultSettings,
|
||||
} from './known-smtp-providers-settings';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import * as SMTPKnownProviders from './known-smtp-providers-settings';
|
||||
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { injectMutation, injectQuery, QueryFunction } from '@tanstack/angular-query-experimental';
|
||||
import { NewAdminService } from '../../services/new-admin.service';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { AddEmailProviderSMTPRequestSchema, GetEmailProviderByIdResponse } from '@zitadel/proto/zitadel/admin_pb';
|
||||
import { EMPTY, map, switchMap } from 'rxjs';
|
||||
import { filter, startWith } from 'rxjs/operators';
|
||||
import { EmailProviderState } from '@zitadel/proto/zitadel/settings_pb';
|
||||
import { ToastService } from '../../services/toast.service';
|
||||
import { MatStepper } from '@angular/material/stepper';
|
||||
import { SMTPConfigState } from 'src/app/proto/generated/zitadel/settings_pb';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ConnectError } from '@zitadel/client';
|
||||
|
||||
type Provider = (typeof SMTPKnownProviders)[keyof typeof SMTPKnownProviders];
|
||||
type State = SMTPProviderComponent['state'] extends Signal<infer T> ? NonNullable<T> : never;
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-smtp-provider',
|
||||
@@ -44,330 +27,489 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
standalone: false,
|
||||
})
|
||||
export class SMTPProviderComponent {
|
||||
public showOptional: boolean = false;
|
||||
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
|
||||
public id: string = '';
|
||||
public providerDefaultSetting: ProviderDefaultSettings = GenericDefaultSettings;
|
||||
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
|
||||
protected readonly EmailProviderState = EmailProviderState;
|
||||
|
||||
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
|
||||
protected readonly emailQuery = this.buildEmailQuery();
|
||||
protected readonly email = linkedSignal(() => this.emailQuery.data() ?? '[email protected]');
|
||||
|
||||
public smtpLoading: boolean = false;
|
||||
public hasSMTPConfig: boolean = false;
|
||||
public isActive: boolean = false;
|
||||
public updateClientSecret: boolean = false;
|
||||
protected readonly newAdminService = inject(NewAdminService);
|
||||
private readonly toast = inject(ToastService);
|
||||
|
||||
// stepper
|
||||
public currentCreateStep: number = 1;
|
||||
public requestRedirectValuesSubject$: Subject<void> = new Subject();
|
||||
public firstFormGroup!: UntypedFormGroup;
|
||||
public secondFormGroup!: UntypedFormGroup;
|
||||
protected readonly router = inject(Router);
|
||||
protected readonly location = inject(Location);
|
||||
private readonly activatedRoute = inject(ActivatedRoute);
|
||||
private readonly fb = inject(FormBuilder);
|
||||
|
||||
public senderEmailPlaceholder = '[email protected]';
|
||||
protected readonly configOrDefaultsQuery: ReturnType<typeof this.buildConfigOrDefaultsQuery>;
|
||||
protected readonly state: ReturnType<typeof this.buildState>;
|
||||
protected readonly updateDataMutation: ReturnType<typeof this.buildUpdateDataMutation>;
|
||||
protected readonly testEmailConfigurationMutation: ReturnType<typeof this.buildTestEmailConfigurationMutation>;
|
||||
|
||||
public resultClass = 'test-success';
|
||||
public isLoading = signal(false);
|
||||
public email: string = '';
|
||||
public testResult: string = '';
|
||||
protected readonly stepper = viewChild(MatStepper);
|
||||
protected readonly preselectedStep: ReturnType<typeof this.getPreselectedStep>;
|
||||
|
||||
constructor(
|
||||
private service: AdminService,
|
||||
private _location: Location,
|
||||
private fb: UntypedFormBuilder,
|
||||
private toast: ToastService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private authService: GrpcAuthService,
|
||||
private translate: TranslateService,
|
||||
) {
|
||||
this.route.parent?.url.subscribe((urlPath) => {
|
||||
const providerName = urlPath[urlPath.length - 1].path;
|
||||
switch (providerName) {
|
||||
case 'aws-ses':
|
||||
this.providerDefaultSetting = AmazonSESDefaultSettings;
|
||||
break;
|
||||
case 'google':
|
||||
this.providerDefaultSetting = GoogleDefaultSettings;
|
||||
break;
|
||||
case 'mailgun':
|
||||
this.providerDefaultSetting = MailgunDefaultSettings;
|
||||
break;
|
||||
case 'mailjet':
|
||||
this.providerDefaultSetting = MailjetDefaultSettings;
|
||||
break;
|
||||
case 'postmark':
|
||||
this.providerDefaultSetting = PostmarkDefaultSettings;
|
||||
break;
|
||||
case 'sendgrid':
|
||||
this.providerDefaultSetting = SendgridDefaultSettings;
|
||||
break;
|
||||
case 'mailchimp':
|
||||
this.providerDefaultSetting = MailchimpDefaultSettings;
|
||||
break;
|
||||
case 'brevo':
|
||||
this.providerDefaultSetting = BrevoDefaultSettings;
|
||||
break;
|
||||
case 'outlook':
|
||||
this.providerDefaultSetting = OutlookDefaultSettings;
|
||||
break;
|
||||
constructor() {
|
||||
this.configOrDefaultsQuery = this.buildConfigOrDefaultsQuery();
|
||||
this.state = this.buildState(this.configOrDefaultsQuery);
|
||||
this.updateDataMutation = this.buildUpdateDataMutation(this.configOrDefaultsQuery, this.state);
|
||||
this.testEmailConfigurationMutation = this.buildTestEmailConfigurationMutation(this.state);
|
||||
|
||||
effect(() => {
|
||||
const error = this.configOrDefaultsQuery.error();
|
||||
if (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
});
|
||||
|
||||
this.preselectedStep = this.getPreselectedStep(this.activatedRoute);
|
||||
}
|
||||
|
||||
private getPreselectedStep(activatedRoute: ActivatedRoute) {
|
||||
const paramMapSignal = toSignal(activatedRoute.paramMap, { requireSync: true });
|
||||
|
||||
return computed(() => {
|
||||
const paramMap = paramMapSignal();
|
||||
const step = paramMap.get('step');
|
||||
if (!step) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.firstFormGroup = this.fb.group({
|
||||
description: [this.providerDefaultSetting.name],
|
||||
tls: [{ value: this.providerDefaultSetting.requiredTls, disabled: this.providerDefaultSetting.requiredTls }],
|
||||
region: [''],
|
||||
hostAndPort: [
|
||||
this.providerDefaultSetting?.host
|
||||
? `${this.providerDefaultSetting?.host}:${this.providerDefaultSetting?.unencryptedPort}`
|
||||
: '',
|
||||
],
|
||||
user: [this.providerDefaultSetting?.user.value || ''],
|
||||
password: [this.providerDefaultSetting?.password.value || ''],
|
||||
});
|
||||
|
||||
this.senderEmailPlaceholder = this.providerDefaultSetting?.senderEmailPlaceholder || '[email protected]';
|
||||
|
||||
this.secondFormGroup = this.fb.group({
|
||||
senderAddress: ['', [requiredValidator]],
|
||||
senderName: ['', [requiredValidator]],
|
||||
replyToAddress: [''],
|
||||
});
|
||||
|
||||
this.region?.valueChanges.subscribe((region: string) => {
|
||||
this.hostAndPort?.setValue(
|
||||
`${region}:${
|
||||
this.tls ? this.providerDefaultSetting?.encryptedPort : this.providerDefaultSetting?.unencryptedPort
|
||||
}`,
|
||||
);
|
||||
});
|
||||
|
||||
if (!this.router.url.endsWith('/create')) {
|
||||
this.id = this.route.snapshot.paramMap.get('id') || '';
|
||||
if (this.id) {
|
||||
this.fetchData(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.authService
|
||||
.getMyUser()
|
||||
.then((resp) => {
|
||||
if (resp.user) {
|
||||
this.email = resp.user.human?.email?.email || '';
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
return Number(step);
|
||||
});
|
||||
}
|
||||
|
||||
public changeStep(event: StepperSelectionEvent): void {
|
||||
this.currentCreateStep = event.selectedIndex + 1;
|
||||
|
||||
if (event.selectedIndex >= 2) {
|
||||
this.requestRedirectValuesSubject$.next();
|
||||
}
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this._location.back();
|
||||
}
|
||||
|
||||
public toggleTLS(event: MatCheckboxChange) {
|
||||
if (this.providerDefaultSetting.host) {
|
||||
this.hostAndPort?.setValue(
|
||||
`${this.providerDefaultSetting?.host}:${
|
||||
event.checked ? this.providerDefaultSetting?.encryptedPort : this.providerDefaultSetting?.unencryptedPort
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private fetchData(id: string): void {
|
||||
this.smtpLoading = true;
|
||||
this.service
|
||||
.getSMTPConfigById(id)
|
||||
.then((data) => {
|
||||
this.smtpLoading = false;
|
||||
if (data.smtpConfig) {
|
||||
this.isActive = data.smtpConfig.state === SMTPConfigState.SMTP_CONFIG_ACTIVE;
|
||||
this.hasSMTPConfig = true;
|
||||
this.firstFormGroup.patchValue({
|
||||
['description']: data.smtpConfig.description,
|
||||
['tls']: data.smtpConfig.tls,
|
||||
['hostAndPort']: data.smtpConfig.host,
|
||||
['user']: data.smtpConfig.user,
|
||||
});
|
||||
this.secondFormGroup.patchValue({
|
||||
['senderAddress']: data.smtpConfig.senderAddress,
|
||||
['senderName']: data.smtpConfig.senderName,
|
||||
['replyToAddress']: data.smtpConfig.replyToAddress,
|
||||
});
|
||||
private buildEmailQuery() {
|
||||
const userQueryOptions = inject(UserService).userQueryOptions();
|
||||
return injectQuery(() => ({
|
||||
...userQueryOptions,
|
||||
select: (user) => {
|
||||
if (user?.type.case !== 'human') {
|
||||
return '';
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.smtpLoading = false;
|
||||
if (error && error.code === 5) {
|
||||
this.hasSMTPConfig = false;
|
||||
}
|
||||
});
|
||||
return user.type.value.email?.email ?? '';
|
||||
},
|
||||
}));
|
||||
}
|
||||
private buildState(configOrDefaultsQuery: typeof this.configOrDefaultsQuery) {
|
||||
const stateSignal = computed(() => {
|
||||
const configOrDefaults = configOrDefaultsQuery.data();
|
||||
if (!configOrDefaults) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return configOrDefaults.case === 'defaults'
|
||||
? ({ ...this.buildFormFromDefaults(configOrDefaults.defaults) } as const)
|
||||
: ({ ...this.buildFormFromConfig(configOrDefaults.config) } as const);
|
||||
});
|
||||
|
||||
const authFormSignal = this.buildAuthForm(stateSignal);
|
||||
|
||||
return computed(() => {
|
||||
const state = stateSignal();
|
||||
const authForm = authFormSignal();
|
||||
|
||||
if (!state || !authForm) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
authForm,
|
||||
} as const;
|
||||
});
|
||||
}
|
||||
|
||||
private updateData(): Promise<UpdateSMTPConfigResponse.AsObject | AddSMTPConfigResponse.AsObject> {
|
||||
if (this.hasSMTPConfig) {
|
||||
const req = new UpdateSMTPConfigRequest();
|
||||
req.setId(this.id);
|
||||
req.setDescription(this.description?.value || '');
|
||||
req.setTls(this.tls?.value ?? false);
|
||||
private readonly hostnameValidator = Validators.pattern(/.+:[0-9]+/);
|
||||
|
||||
if (this.hostAndPort && this.hostAndPort.value) {
|
||||
req.setHost(this.hostAndPort.value);
|
||||
private buildFormFromConfig(config: ReturnType<typeof this.getConfig>) {
|
||||
const mainForm = this.fb.group({
|
||||
description: new FormControl<string>(config.description, {
|
||||
nonNullable: true,
|
||||
validators: [requiredValidator],
|
||||
}),
|
||||
user: new FormControl<string>(config.config.value.user, { nonNullable: true, validators: [requiredValidator] }),
|
||||
host: new FormControl(config.config.value.host, {
|
||||
nonNullable: true,
|
||||
validators: [this.hostnameValidator],
|
||||
}),
|
||||
tls: new FormControl(config.config.value.tls, { nonNullable: true }),
|
||||
xoauth2: new FormControl<boolean>(config.config.value.Auth.case === 'xoauth2', { nonNullable: true }),
|
||||
});
|
||||
|
||||
mainForm.controls.xoauth2.disable();
|
||||
|
||||
const senderForm = this.buildSenderForm(config.config.value);
|
||||
|
||||
return {
|
||||
mainForm,
|
||||
senderForm,
|
||||
senderEmailPlaceholder: '[email protected]',
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
private buildFormFromDefaults(defaults?: Provider):
|
||||
| {
|
||||
mainForm: typeof mainForm;
|
||||
senderForm: typeof senderForm;
|
||||
senderEmailPlaceholder: string;
|
||||
}
|
||||
if (this.user && this.user.value) {
|
||||
req.setUser(this.user.value);
|
||||
}
|
||||
if (this.password && this.password.value) {
|
||||
req.setPassword(this.password.value);
|
||||
}
|
||||
if (this.senderAddress && this.senderAddress.value) {
|
||||
req.setSenderAddress(this.senderAddress.value);
|
||||
}
|
||||
if (this.senderName && this.senderName.value) {
|
||||
req.setSenderName(this.senderName.value);
|
||||
}
|
||||
if (this.replyToAddress && this.replyToAddress.value) {
|
||||
req.setReplyToAddress(this.replyToAddress.value);
|
||||
}
|
||||
return this.service.updateSMTPConfig(req);
|
||||
} else {
|
||||
const req = new AddSMTPConfigRequest();
|
||||
req.setDescription(this.description?.value ?? '');
|
||||
req.setHost(this.hostAndPort?.value ?? '');
|
||||
req.setSenderAddress(this.senderAddress?.value ?? '');
|
||||
req.setSenderName(this.senderName?.value ?? '');
|
||||
req.setReplyToAddress(this.replyToAddress?.value ?? '');
|
||||
req.setTls(this.tls?.value ?? false);
|
||||
req.setUser(this.user?.value ?? '');
|
||||
req.setPassword(this.password?.value ?? '');
|
||||
return this.service.addSMTPConfig(req);
|
||||
| {
|
||||
mainForm: typeof mainForm;
|
||||
senderForm: typeof senderForm;
|
||||
senderEmailPlaceholder: string;
|
||||
defaults: Provider;
|
||||
} {
|
||||
const mainForm = this.fb.group({
|
||||
description: new FormControl<string>(defaults?.description ?? '', {
|
||||
nonNullable: true,
|
||||
validators: [requiredValidator],
|
||||
}),
|
||||
user: new FormControl<string>(defaults?.user.value ?? '', { nonNullable: true, validators: [requiredValidator] }),
|
||||
host: new FormControl(defaults?.host ?? '', {
|
||||
nonNullable: true,
|
||||
validators: [this.hostnameValidator],
|
||||
}),
|
||||
tls: new FormControl<boolean>(true, { nonNullable: true }),
|
||||
xoauth2: new FormControl<boolean>(defaults?.auth.case === 'xoauth2', { nonNullable: true }),
|
||||
});
|
||||
|
||||
if (defaults) {
|
||||
mainForm.controls.tls.disable();
|
||||
mainForm.controls.xoauth2.disable();
|
||||
}
|
||||
|
||||
const senderForm = this.buildSenderForm();
|
||||
const senderEmailPlaceholder =
|
||||
defaults && 'senderEmailPlaceholder' in defaults ? defaults.senderEmailPlaceholder : '[email protected]';
|
||||
|
||||
return defaults
|
||||
? {
|
||||
mainForm,
|
||||
senderForm,
|
||||
senderEmailPlaceholder,
|
||||
defaults,
|
||||
}
|
||||
: {
|
||||
mainForm,
|
||||
senderForm,
|
||||
senderEmailPlaceholder,
|
||||
};
|
||||
}
|
||||
|
||||
public activateSMTPConfig() {
|
||||
this.service
|
||||
.activateSMTPConfig(this.id)
|
||||
.then(() => {
|
||||
this.toast.showInfo('SMTP.LIST.DIALOG.ACTIVATED', true);
|
||||
this.isActive = true;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
private buildSenderForm(config?: { senderAddress: string; senderName: string; replyToAddress: string }) {
|
||||
return this.fb.group({
|
||||
senderAddress: new FormControl(config?.senderAddress ?? '', { nonNullable: true, validators: [requiredValidator] }),
|
||||
senderName: new FormControl(config?.senderName ?? '', { nonNullable: true, validators: [requiredValidator] }),
|
||||
replyToAddress: new FormControl(config?.replyToAddress ?? '', { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
public deactivateSMTPConfig() {
|
||||
this.service
|
||||
.deactivateSMTPConfig(this.id)
|
||||
.then(() => {
|
||||
this.toast.showInfo('SMTP.LIST.DIALOG.DEACTIVATED', true);
|
||||
this.isActive = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
private buildAuthForm(
|
||||
stateSignal: Signal<
|
||||
ReturnType<typeof this.buildFormFromDefaults> | ReturnType<typeof this.buildFormFromConfig> | undefined
|
||||
>,
|
||||
) {
|
||||
const xoauth2$ = toObservable(stateSignal).pipe(
|
||||
switchMap((state) => {
|
||||
if (!state) {
|
||||
return EMPTY;
|
||||
}
|
||||
const xoauth2 = state.mainForm.controls.xoauth2;
|
||||
return xoauth2.valueChanges.pipe(startWith(xoauth2.value));
|
||||
}),
|
||||
);
|
||||
|
||||
public savePolicy(stepper: MatStepper): void {
|
||||
this.updateData()
|
||||
.then((resp) => {
|
||||
if (!this.id) {
|
||||
// This is a new SMTP provider let's get the ID from the addSMTPConfig response
|
||||
let createResponse = resp as AddSMTPConfigResponse.AsObject;
|
||||
this.id = createResponse.id;
|
||||
const xoauth2Signal = toSignal(xoauth2$);
|
||||
|
||||
return computed(() => {
|
||||
const state = stateSignal();
|
||||
const xoauth2 = xoauth2Signal();
|
||||
if (!state || xoauth2 === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!xoauth2) {
|
||||
const form = this.fb.group({
|
||||
password: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: 'defaults' in state ? [requiredValidator] : [],
|
||||
}),
|
||||
});
|
||||
|
||||
if ('config' in state) {
|
||||
form.controls.password.disable();
|
||||
}
|
||||
|
||||
this.toast.showInfo('SETTING.SMTP.SAVED', true);
|
||||
setTimeout(() => {
|
||||
stepper.next();
|
||||
}, 2000);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (`${error}`.includes('No changes')) {
|
||||
return form;
|
||||
}
|
||||
|
||||
const defaultValues =
|
||||
'config' in state && state.config.config.value.Auth.case === 'xoauth2'
|
||||
? {
|
||||
tokenEndpoint: state.config.config.value.Auth.value.tokenEndpoint,
|
||||
scopes: state.config.config.value.Auth.value.scopes.join(','),
|
||||
clientId: state.config.config.value.Auth.value.OAuth2Type.value?.clientId ?? '',
|
||||
}
|
||||
: 'defaults' in state && state.defaults.auth.case === 'xoauth2'
|
||||
? { scopes: state.defaults.auth.scopes }
|
||||
: {};
|
||||
|
||||
const form = this.fb.group({
|
||||
tokenEndpoint: new FormControl<string>(defaultValues.tokenEndpoint ?? '', {
|
||||
nonNullable: true,
|
||||
validators: [requiredValidator],
|
||||
}),
|
||||
scopes: new FormControl<string>(defaultValues.scopes ?? '', { nonNullable: true, validators: [requiredValidator] }),
|
||||
clientId: new FormControl<string>(defaultValues.clientId ?? '', {
|
||||
nonNullable: true,
|
||||
validators: [requiredValidator],
|
||||
}),
|
||||
clientSecret: new FormControl<string>('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
});
|
||||
|
||||
if ('config' in state) {
|
||||
form.controls.tokenEndpoint.disable();
|
||||
form.controls.scopes.disable();
|
||||
form.controls.clientId.disable();
|
||||
form.controls.clientSecret.disable();
|
||||
}
|
||||
|
||||
return form;
|
||||
});
|
||||
}
|
||||
|
||||
private buildConfigOrDefaultsQuery() {
|
||||
const idOrProvider$ = this.activatedRoute.paramMap.pipe(
|
||||
map((params) => params.get('provider')),
|
||||
filter(Boolean),
|
||||
);
|
||||
|
||||
const idOrProviderSignal = toSignal(idOrProvider$, { requireSync: true });
|
||||
|
||||
return injectQuery(() => {
|
||||
const idOrProvider = idOrProviderSignal();
|
||||
|
||||
const query = this.newAdminService.getEmailProviderByIdQueryOptions(idOrProvider);
|
||||
const queryKey = query.queryKey as (string | undefined)[];
|
||||
const queryFn = query.queryFn as QueryFunction<GetEmailProviderByIdResponse | string>;
|
||||
|
||||
const select = (configOrProvider: GetEmailProviderByIdResponse | string) =>
|
||||
typeof configOrProvider === 'string'
|
||||
? ({
|
||||
case: 'defaults',
|
||||
defaults: SMTPKnownProviders[configOrProvider as keyof typeof SMTPKnownProviders] as
|
||||
| (typeof SMTPKnownProviders)[keyof typeof SMTPKnownProviders]
|
||||
| undefined,
|
||||
} as const)
|
||||
: ({ case: 'config', config: this.getConfig(configOrProvider) } as const);
|
||||
|
||||
if (idOrProvider in SMTPKnownProviders || idOrProvider === 'generic') {
|
||||
return {
|
||||
queryKey,
|
||||
queryFn: (async () => idOrProvider) as typeof queryFn,
|
||||
gcTime: 0,
|
||||
select,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
select,
|
||||
} as const;
|
||||
});
|
||||
}
|
||||
|
||||
private getConfig(resp: GetEmailProviderByIdResponse) {
|
||||
if (!resp.config) {
|
||||
throw new Error('No SMTP provider config found');
|
||||
}
|
||||
|
||||
if (resp.config.config.case !== 'smtp') {
|
||||
throw new Error('Email provider config with id ' + resp.config.id + ' is not an SMTP config');
|
||||
}
|
||||
|
||||
const config = resp.config.config.value;
|
||||
|
||||
return {
|
||||
...resp.config,
|
||||
config: {
|
||||
case: 'smtp' as const,
|
||||
value: {
|
||||
...config,
|
||||
Auth: config.Auth,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private buildUpdateDataMutation(configOrDefaultsQuery: typeof this.configOrDefaultsQuery, stateSignal: typeof this.state) {
|
||||
return injectMutation(() => {
|
||||
const state = stateSignal();
|
||||
const stepper = this.stepper();
|
||||
|
||||
return {
|
||||
mutationFn: () => {
|
||||
if (!state) {
|
||||
throw new Error('Invalid state');
|
||||
}
|
||||
return this.updateData(state);
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.toast.showInfo('SETTING.SMTP.SAVED', true);
|
||||
stepper?.next();
|
||||
},
|
||||
onError: (error: ConnectError) => {
|
||||
if (!error.message.includes('No changes')) {
|
||||
this.toast.showError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.toast.showInfo('SETTING.SMTP.NOCHANGES', true);
|
||||
setTimeout(() => {
|
||||
stepper.next();
|
||||
}, 2000);
|
||||
} else {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
stepper?.next();
|
||||
},
|
||||
onSettled: () => configOrDefaultsQuery.refetch(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async updateData(state: State) {
|
||||
const authValues = state.authForm.getRawValue();
|
||||
|
||||
const { user, tls, host, description } = state.mainForm.getRawValue();
|
||||
const { senderAddress, senderName, replyToAddress } = state.senderForm.getRawValue();
|
||||
|
||||
if ('config' in state) {
|
||||
return this.newAdminService.updateEmailProviderSMTP({
|
||||
id: state.config.id,
|
||||
description,
|
||||
senderAddress,
|
||||
senderName,
|
||||
replyToAddress,
|
||||
host,
|
||||
user,
|
||||
tls,
|
||||
});
|
||||
}
|
||||
|
||||
const Auth: MessageInitShape<typeof AddEmailProviderSMTPRequestSchema>['Auth'] =
|
||||
'tokenEndpoint' in authValues
|
||||
? {
|
||||
case: 'xoauth2',
|
||||
value: {
|
||||
tokenEndpoint: authValues.tokenEndpoint,
|
||||
scopes: authValues.scopes.replace(/\s/g, '').split(','),
|
||||
OAuth2Type: {
|
||||
case: 'clientCredentials',
|
||||
value: {
|
||||
clientId: authValues.clientId,
|
||||
clientSecret: authValues.clientSecret,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: authValues.password
|
||||
? { case: 'plain', value: { password: authValues.password } }
|
||||
: { case: 'none', value: {} };
|
||||
|
||||
const res = await this.newAdminService.addEmailProviderSMTP({
|
||||
senderAddress,
|
||||
senderName,
|
||||
description,
|
||||
replyToAddress,
|
||||
host,
|
||||
user,
|
||||
tls,
|
||||
Auth,
|
||||
});
|
||||
|
||||
await this.router.navigate(['/instance/smtpprovider', res.id, { step: 3 }], { skipLocationChange: true });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public testEmailConfiguration(): void {
|
||||
this.isLoading.set(true);
|
||||
|
||||
const req = new TestSMTPConfigRequest();
|
||||
req.setSenderAddress(this.senderAddress?.value ?? '');
|
||||
req.setSenderName(this.senderName?.value ?? '');
|
||||
req.setHost(this.hostAndPort?.value ?? '');
|
||||
req.setUser(this.user?.value);
|
||||
req.setPassword(this.password?.value ?? '');
|
||||
req.setTls(this.tls?.value ?? false);
|
||||
req.setId(this.id ?? '');
|
||||
req.setReceiverAddress(this.email ?? '');
|
||||
|
||||
this.service
|
||||
.testSMTPConfig(req)
|
||||
.then(() => {
|
||||
this.resultClass = 'test-success';
|
||||
this.isLoading.set(false);
|
||||
this.translate
|
||||
.get('SMTP.CREATE.STEPS.TEST.RESULT')
|
||||
.pipe(take(1))
|
||||
.subscribe((msg) => {
|
||||
this.testResult = msg;
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.resultClass = 'test-error';
|
||||
this.isLoading.set(false);
|
||||
this.testResult = error;
|
||||
});
|
||||
protected async activateSMTPConfig(id: string) {
|
||||
try {
|
||||
await this.newAdminService.activateSMTPConfig(id);
|
||||
this.toast.showInfo('SMTP.LIST.DIALOG.ACTIVATED', true);
|
||||
await this.configOrDefaultsQuery.refetch();
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public get description(): AbstractControl | null {
|
||||
return this.firstFormGroup.get('description');
|
||||
protected async deactivateSMTPConfig(id: string) {
|
||||
try {
|
||||
await this.newAdminService.deactivateSMTPConfig(id);
|
||||
this.toast.showInfo('SMTP.LIST.DIALOG.DEACTIVATED', true);
|
||||
await this.configOrDefaultsQuery.refetch();
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public get tls(): AbstractControl | null {
|
||||
return this.firstFormGroup.get('tls');
|
||||
}
|
||||
protected buildTestEmailConfigurationMutation(stateSignal: typeof this.state) {
|
||||
const buildRequest = (state: State, receiverAddress: string) => {
|
||||
const authValues = state.authForm.getRawValue();
|
||||
const { user, tls, host } = state.mainForm.getRawValue();
|
||||
const { senderAddress, senderName } = state.senderForm.getRawValue();
|
||||
|
||||
public get region(): AbstractControl | null {
|
||||
return this.firstFormGroup.get('region');
|
||||
}
|
||||
if ('config' in state) {
|
||||
return {
|
||||
id: state.config.id,
|
||||
senderAddress,
|
||||
senderName,
|
||||
host,
|
||||
user,
|
||||
tls,
|
||||
receiverAddress,
|
||||
};
|
||||
}
|
||||
|
||||
public get hostAndPort(): AbstractControl | null {
|
||||
return this.firstFormGroup.get('hostAndPort');
|
||||
}
|
||||
const Auth: MessageInitShape<typeof AddEmailProviderSMTPRequestSchema>['Auth'] =
|
||||
'tokenEndpoint' in authValues
|
||||
? {
|
||||
case: 'xoauth2',
|
||||
value: {
|
||||
tokenEndpoint: authValues.tokenEndpoint,
|
||||
scopes: authValues.scopes.replace(/\s/g, '').split(','),
|
||||
OAuth2Type: {
|
||||
case: 'clientCredentials',
|
||||
value: {
|
||||
clientId: authValues.clientId,
|
||||
clientSecret: authValues.clientSecret,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: authValues.password
|
||||
? { case: 'plain', value: { password: authValues.password } }
|
||||
: { case: 'none', value: {} };
|
||||
|
||||
public get user(): AbstractControl | null {
|
||||
return this.firstFormGroup.get('user');
|
||||
}
|
||||
return {
|
||||
senderAddress,
|
||||
senderName,
|
||||
host,
|
||||
user,
|
||||
tls,
|
||||
Auth,
|
||||
receiverAddress,
|
||||
};
|
||||
};
|
||||
|
||||
public get password(): AbstractControl | null {
|
||||
return this.firstFormGroup.get('password');
|
||||
}
|
||||
return injectMutation(() => {
|
||||
const state = stateSignal();
|
||||
const email = this.email();
|
||||
|
||||
public get senderAddress(): AbstractControl | null {
|
||||
return this.secondFormGroup.get('senderAddress');
|
||||
}
|
||||
|
||||
public get senderName(): AbstractControl | null {
|
||||
return this.secondFormGroup.get('senderName');
|
||||
}
|
||||
|
||||
public get replyToAddress(): AbstractControl | null {
|
||||
return this.secondFormGroup.get('replyToAddress');
|
||||
return {
|
||||
mutationFn: () => {
|
||||
if (!state) {
|
||||
throw new Error('Invalid state');
|
||||
}
|
||||
return this.newAdminService.testEmailProviderSMTP(buildRequest(state, email));
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user