Files
zitadel/console/src/app/modules/smtp-provider/smtp-provider.component.ts
T
a1a4572303 chore(deps): update npm dependencies and clean up pnpm overrides (#12592)
# Which Problems Are Solved

Several dependencies across the workspace had fallen behind their latest
patch releases, and the override list in pnpm-workspace.yaml had
drifted: many of the 28 entries were no longer needed because the
dependency tree had already moved past them, while a few enforced
outdated version floors.

# How the Problems Are Solved

Direct dependencies were updated where newer patch releases were
available: `@angular/*` → ^21.2.19, `next` → 16.2.11 (docs and login,
incl. `@next/eslint-plugin-next`), `nx` → 22.7.7, and `js-yaml` → ^4.3.1
/ `postcss` → ^8.5.23 in docs. The lockfile was regenerated from scratch
so all transitive dependencies resolve to the newest versions within
their allowed ranges. With that in place, the override list was rebuilt
down to the 8 entries that are still required because a parent package
pins an older version (axios and brace-expansion via nx, postcss and
sharp via next, esbuild via tsx, uuid via testcontainers, and the two
OpenTelemetry packages); the other 20 overrides were removed. A
verification pass confirmed the resolved versions stay at or above the
floors the removed overrides used to enforce, and `pnpm audit` comes
back clean.

---------

Co-authored-by: Livio Spring <[email protected]>
2026-08-19 06:30:10 +00:00

515 lines
16 KiB
TypeScript

import { Location } from '@angular/common';
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 { ActivatedRoute, Router } from '@angular/router';
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 { 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',
templateUrl: './smtp-provider.component.html',
styleUrls: ['./smtp-provider.scss'],
standalone: false,
})
export class SMTPProviderComponent {
protected readonly EmailProviderState = EmailProviderState;
protected readonly emailQuery = this.buildEmailQuery();
protected readonly email = linkedSignal(() => this.emailQuery.data() ?? '[email protected]');
protected readonly newAdminService = inject(NewAdminService);
private readonly toast = inject(ToastService);
protected readonly router = inject(Router);
protected readonly location = inject(Location);
private readonly activatedRoute = inject(ActivatedRoute);
private readonly fb = inject(FormBuilder);
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>;
protected readonly stepper = viewChild(MatStepper);
protected readonly preselectedStep: ReturnType<typeof this.getPreselectedStep>;
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;
}
return Number(step);
});
}
private buildEmailQuery() {
const userQueryOptions = inject(UserService).userQueryOptions();
return injectQuery(() => ({
...userQueryOptions,
select: (user) => {
if (user?.type.case !== 'human') {
return '';
}
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 readonly hostnameValidator = Validators.pattern(/.+:[0-9]+/);
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;
}
| {
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,
};
}
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 }),
});
}
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));
}),
);
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();
}
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);
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;
}
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);
}
}
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);
}
}
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();
if ('config' in state) {
return {
id: state.config.id,
senderAddress,
senderName,
host,
user,
tls,
receiverAddress,
};
}
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: {} };
return {
senderAddress,
senderName,
host,
user,
tls,
Auth,
receiverAddress,
};
};
return injectMutation(() => {
const state = stateSignal();
const email = this.email();
return {
mutationFn: () => {
if (!state) {
throw new Error('Invalid state');
}
return this.newAdminService.testEmailProviderSMTP(buildRequest(state, email));
},
};
});
}
}