From ceffb9c5363e3cb1e62302df42729a4b4cb5225e Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 11 Feb 2026 18:12:41 +0100 Subject: [PATCH] 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 --- .../notification-smtp-provider.component.html | 48 +- .../notification-smtp-provider.component.ts | 35 +- .../notification-smtp-provider.module.ts | 33 - .../settings-list.component.html | 2 +- .../settings-list/settings-list.module.ts | 5 +- .../known-smtp-providers-settings.ts | 266 +++--- .../smtp-provider-routing.module.ts | 30 +- .../smtp-provider.component.html | 554 ++++++------ .../smtp-provider/smtp-provider.component.ts | 786 +++++++++++------- .../smtp-table/smtp-table.component.ts | 8 - console/src/app/services/new-admin.service.ts | 39 +- .../e2e/instance/settings/notifications.cy.ts | 36 +- tests/functional-ui/project.json | 2 +- 13 files changed, 975 insertions(+), 869 deletions(-) delete mode 100644 console/src/app/modules/policies/notification-smtp-provider/notification-smtp-provider.module.ts diff --git a/console/src/app/modules/policies/notification-smtp-provider/notification-smtp-provider.component.html b/console/src/app/modules/policies/notification-smtp-provider/notification-smtp-provider.component.html index 5f86d2300..fafb10215 100644 --- a/console/src/app/modules/policies/notification-smtp-provider/notification-smtp-provider.component.html +++ b/console/src/app/modules/policies/notification-smtp-provider/notification-smtp-provider.component.html @@ -2,36 +2,32 @@

{{ 'SMTP.LIST.DESCRIPTION' | translate }}

- +

{{ 'SMTP.CREATE.TITLE' | translate }}

{{ 'SMTP.CREATE.DESCRIPTION' | translate }}

-
- - -
- {{ provider.name | titlecase }} -
-
- - -
- -
-
- Generic SMTP -
-
-
+ @for (providerEntry of providers | keyvalue; track providerEntry.key) { + @let id = providerEntry.key; + @let provider = providerEntry.value; +
+ + @if (provider.description !== 'generic') { + +
+ {{ provider.description | titlecase }} +
+ } @else { +
+ +
+
+ Generic SMTP +
+ } +
+
+ }
diff --git a/console/src/app/modules/policies/notification-smtp-provider/notification-smtp-provider.component.ts b/console/src/app/modules/policies/notification-smtp-provider/notification-smtp-provider.component.ts index 5d9fc2414..16eee4890 100644 --- a/console/src/app/modules/policies/notification-smtp-provider/notification-smtp-provider.component.ts +++ b/console/src/app/modules/policies/notification-smtp-provider/notification-smtp-provider.component.ts @@ -1,33 +1,20 @@ -import { Component, Injector, Input, OnInit, Type } from '@angular/core'; -import { AdminService } from 'src/app/services/admin.service'; -import { ManagementService } from 'src/app/services/mgmt.service'; +import { Component } from '@angular/core'; import { PolicyComponentServiceType } from '../policy-component-types.enum'; -import { SMTPKnownProviders } from '../../smtp-provider/known-smtp-providers-settings'; +import * as SMTPKnownProviders from '../../smtp-provider/known-smtp-providers-settings'; +import { TranslatePipe } from '@ngx-translate/core'; +import { SMTPTableModule } from '../../smtp-table/smtp-table.module'; +import { RouterLink } from '@angular/router'; +import { KeyValuePipe, TitleCasePipe } from '@angular/common'; +import { MatIcon } from '@angular/material/icon'; @Component({ selector: 'cnsl-notification-smtp-provider', templateUrl: './notification-smtp-provider.component.html', styleUrls: ['./notification-smtp-provider.component.scss'], - standalone: false, + imports: [TranslatePipe, SMTPTableModule, RouterLink, TitleCasePipe, MatIcon, KeyValuePipe], }) -export class NotificationSMTPProviderComponent implements OnInit { - @Input() public serviceType!: PolicyComponentServiceType; - public service!: ManagementService | AdminService; - - public PolicyComponentServiceType: any = PolicyComponentServiceType; - public providers = SMTPKnownProviders; - - constructor(private injector: Injector) {} - - ngOnInit(): void { - switch (this.serviceType) { - case PolicyComponentServiceType.MGMT: - this.service = this.injector.get(ManagementService as Type); - break; - case PolicyComponentServiceType.ADMIN: - this.service = this.injector.get(AdminService as Type); - break; - } - } +export class NotificationSMTPProviderComponent { + protected readonly PolicyComponentServiceType = PolicyComponentServiceType; + protected readonly providers = { ...SMTPKnownProviders, generic: { description: 'generic' } } as const; } diff --git a/console/src/app/modules/policies/notification-smtp-provider/notification-smtp-provider.module.ts b/console/src/app/modules/policies/notification-smtp-provider/notification-smtp-provider.module.ts deleted file mode 100644 index 4dbf10963..000000000 --- a/console/src/app/modules/policies/notification-smtp-provider/notification-smtp-provider.module.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { MatIconModule } from '@angular/material/icon'; -import { RouterModule } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; -import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; - -import { CardModule } from '../../card/card.module'; -import { NotificationSMTPProviderComponent } from './notification-smtp-provider.component'; -import { InputModule } from '../../input/input.module'; -import { FormFieldModule } from '../../form-field/form-field.module'; -import { SMTPTableModule } from '../../smtp-table/smtp-table.module'; -import { MatButtonModule } from '@angular/material/button'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; - -@NgModule({ - declarations: [NotificationSMTPProviderComponent], - imports: [ - InputModule, - FormFieldModule, - CommonModule, - MatButtonModule, - CardModule, - MatIconModule, - SMTPTableModule, - RouterModule, - HasRolePipeModule, - MatProgressSpinnerModule, - TranslateModule, - ], - exports: [NotificationSMTPProviderComponent], -}) -export class NotificationSMTPProviderModule {} diff --git a/console/src/app/modules/settings-list/settings-list.component.html b/console/src/app/modules/settings-list/settings-list.component.html index eabd07066..db884517d 100644 --- a/console/src/app/modules/settings-list/settings-list.component.html +++ b/console/src/app/modules/settings-list/settings-list.component.html @@ -27,7 +27,7 @@ - + diff --git a/console/src/app/modules/settings-list/settings-list.module.ts b/console/src/app/modules/settings-list/settings-list.module.ts index c6abe310c..440b53e65 100644 --- a/console/src/app/modules/settings-list/settings-list.module.ts +++ b/console/src/app/modules/settings-list/settings-list.module.ts @@ -28,12 +28,12 @@ import FailedEventsModule from '../failed-events/failed-events.module'; import IamViewsModule from '../iam-views/iam-views.module'; import EventsModule from '../events/events.module'; import { OrgTableModule } from '../org-table/org-table.module'; -import { NotificationSMTPProviderModule } from '../policies/notification-smtp-provider/notification-smtp-provider.module'; import { FeaturesComponent } from 'src/app/components/features/features.component'; import OrgListModule from 'src/app/pages/org-list/org-list.module'; import ActionsTwoModule from '../actions-two/actions-two.module'; import { provideRouter } from '@angular/router'; import { OidcWebkeysModule } from '../policies/oidc-webkeys/oidc-webkeys.module'; +import { NotificationSMTPProviderComponent } from '../policies/notification-smtp-provider/notification-smtp-provider.component'; @NgModule({ declarations: [SettingsListComponent], @@ -51,7 +51,7 @@ import { OidcWebkeysModule } from '../policies/oidc-webkeys/oidc-webkeys.module' LanguageSettingsModule, NotificationPolicyModule, IdpSettingsModule, - NotificationSMTPProviderModule, + NotificationSMTPProviderComponent, PrivacyPolicyModule, MessageTextsPolicyModule, SecurityPolicyModule, @@ -63,7 +63,6 @@ import { OidcWebkeysModule } from '../policies/oidc-webkeys/oidc-webkeys.module' TranslateModule, HasRolePipeModule, FeaturesComponent, - NotificationSMTPProviderModule, NotificationSMSProviderModule, OIDCConfigurationModule, OidcWebkeysModule, diff --git a/console/src/app/modules/smtp-provider/known-smtp-providers-settings.ts b/console/src/app/modules/smtp-provider/known-smtp-providers-settings.ts index c0d8f7e28..151178476 100644 --- a/console/src/app/modules/smtp-provider/known-smtp-providers-settings.ts +++ b/console/src/app/modules/smtp-provider/known-smtp-providers-settings.ts @@ -1,192 +1,146 @@ -export interface AmazonRegionsEndpoints { - 'US East (Ohio)': string; - 'US East (N. Virginia)': string; - 'US West (N. California)': string; - 'US West (Oregon)': string; - 'Asia Pacific (Mumbai)': string; - 'Asia Pacific (Osaka)': string; - 'Asia Pacific (Seoul)': string; - 'Asia Pacific (Singapore)': string; - 'Asia Pacific (Sydney)': string; - 'Asia Pacific (Tokyo)': string; - 'Canada (Central)': string; - 'Europe (Frankfurt)': string; - 'Europe (London)': string; - 'Europe (Paris)': string; - 'Europe (Stockholm)': string; - 'South America (São Paulo)': string; -} - -const amazonEndpoints = { - 'US East (Ohio)': 'email-smtp.us-east-2.amazonaws.com', - 'US East (N. Virginia)': 'email-smtp.us-east-1.amazonaws.com', - 'US West (N. California)': 'email-smtp.us-west-1.amazonaws.com', - 'US West (Oregon)': 'email-smtp.us-west-2.amazonaws.com', - 'Asia Pacific (Mumbai)': 'email-smtp.ap-south-1.amazonaws.com', - 'Asia Pacific (Osaka)': 'email-smtp.ap-northeast-3.amazonaws.com', - 'Asia Pacific (Seoul)': 'email-smtp.ap-northeast-2.amazonaws.com', - 'Asia Pacific (Singapore)': 'email-smtp.ap-southeast-1.amazonaws.com', - 'Asia Pacific (Sydney)': 'email-smtp.ap-southeast-2.amazonaws.com', - 'Asia Pacific (Tokyo)': 'email-smtp.ap-northeast-1.amazonaws.com', - 'Canada (Central)': 'email-smtp.ca-central-1.amazonaws.com', - 'Europe (Frankfurt)': 'email-smtp.eu-central-1.amazonaws.com', - 'Europe (Ireland)': 'email-smtp.eu-west-1.amazonaws.com', - 'Europe (London)': 'email-smtp.eu-west-2.amazonaws.com', - 'Europe (Paris)': 'email-smtp.eu-west-3.amazonaws.com', - 'Europe (Stockholm)': 'email-smtp.eu-north-1.amazonaws.com', - 'South America (São Paulo)': 'email-smtp.sa-east-1.amazonaws.com', -}; - -export interface ProviderDefaultSettings { - name: string; - regions?: AmazonRegionsEndpoints; - multiHostsLabel?: string; - requiredTls: boolean; - host?: string; - unencryptedPort?: number; - encryptedPort?: number; +type ProviderDefaultSettings = { + description: string; + host: string; user: { value: string; placeholder: string; }; - password: { - value: string; - placeholder: string; - }; + auth: + | { + case: 'plain'; + password: { + value: string; + placeholder: string; + }; + } + | { + case: 'xoauth2'; + scopes: string; + }; senderEmailPlaceholder?: string; - image?: string; - routerLinkElement: string; -} + image: string; +}; -export const AmazonSESDefaultSettings: ProviderDefaultSettings = { - name: 'amazon SES', - regions: amazonEndpoints, - multiHostsLabel: 'Choose your region', - requiredTls: true, - encryptedPort: 587, +export const amazon = { + description: 'amazon SES', + host: 'email-smtp.us-east-2.amazonaws.com:587', user: { value: '', placeholder: 'your Amazon SES credentials for this region' }, - password: { value: '', placeholder: 'your Amazon SES credentials for this region' }, + auth: { + case: 'plain', + password: { value: '', placeholder: 'your Amazon SES credentials for this region' }, + }, image: './assets/images/smtp/aws-ses.svg', - routerLinkElement: 'aws-ses', -}; +} as const; -export const GoogleDefaultSettings: ProviderDefaultSettings = { - name: 'google', - requiredTls: true, - host: 'smtp.gmail.com', - unencryptedPort: 587, - encryptedPort: 587, +amazon satisfies ProviderDefaultSettings; + +export const google = { + description: 'google', + host: 'smtp.gmail.com:587', user: { value: '', placeholder: 'your complete Google Workspace email address' }, - password: { value: '', placeholder: 'your complete Google Workspace password' }, + auth: { + case: 'plain', + password: { value: '', placeholder: 'your complete Google Workspace password' }, + }, image: './assets/images/smtp/google.png', - routerLinkElement: 'google', -}; +} as const; -export const MailgunDefaultSettings: ProviderDefaultSettings = { - name: 'mailgun', - requiredTls: false, - host: 'smtp.mailgun.org', - unencryptedPort: 587, - encryptedPort: 465, +google satisfies ProviderDefaultSettings; + +export const mailgun = { + description: 'mailgun', + host: 'smtp.mailgun.org:465', user: { value: '', placeholder: 'postmaster@YOURDOMAIN' }, - password: { value: '', placeholder: 'Your mailgun smtp password' }, + auth: { + case: 'plain', + password: { value: '', placeholder: 'Your mailgun smtp password' }, + }, image: './assets/images/smtp/mailgun.svg', - routerLinkElement: 'mailgun', -}; +} as const; -export const MailjetDefaultSettings: ProviderDefaultSettings = { - name: 'mailjet', - requiredTls: false, - host: 'in-v3.mailjet.com', - unencryptedPort: 587, - encryptedPort: 465, +mailgun satisfies ProviderDefaultSettings; + +export const mailjet = { + description: 'mailjet', + host: 'in-v3.mailjet.com:465', user: { value: '', placeholder: 'Your Mailjet API key' }, - password: { value: '', placeholder: 'Your Mailjet Secret key' }, + auth: { + case: 'plain', + password: { value: '', placeholder: 'Your Mailjet Secret key' }, + }, image: './assets/images/smtp/mailjet.svg', senderEmailPlaceholder: 'An authorized domain or email address', - routerLinkElement: 'mailjet', -}; +} as const; -export const PostmarkDefaultSettings: ProviderDefaultSettings = { - name: 'postmark', - requiredTls: false, - host: 'smtp.postmarkapp.com', - unencryptedPort: 587, - encryptedPort: 587, +mailjet satisfies ProviderDefaultSettings; + +export const postmark = { + description: 'postmark', + host: 'smtp.postmarkapp.com:587', user: { value: '', placeholder: 'Your Server API token' }, - password: { value: '', placeholder: 'Your Server API token' }, + auth: { + case: 'plain', + password: { value: '', placeholder: 'Your Server API token' }, + }, image: './assets/images/smtp/postmark.png', senderEmailPlaceholder: 'An authorized domain or email address', - routerLinkElement: 'postmark', -}; +} as const; -export const SendgridDefaultSettings: ProviderDefaultSettings = { - name: 'sendgrid', - requiredTls: false, - host: 'smtp.sendgrid.net', - unencryptedPort: 587, - encryptedPort: 465, +postmark satisfies ProviderDefaultSettings; + +export const sendgrid = { + description: 'sendgrid', + host: 'smtp.sendgrid.net:465', user: { value: 'apikey', placeholder: '' }, - password: { value: '', placeholder: ' Your SendGrid API Key' }, + auth: { + case: 'plain', + password: { value: '', placeholder: ' Your SendGrid API Key' }, + }, image: './assets/images/smtp/sendgrid.png', - routerLinkElement: 'sendgrid', -}; +} as const; -export const MailchimpDefaultSettings: ProviderDefaultSettings = { - name: 'mailchimp', - requiredTls: false, - host: 'smtp.mandrillapp.com', - unencryptedPort: 587, - encryptedPort: 465, +sendgrid satisfies ProviderDefaultSettings; + +export const mailchimp = { + description: 'mailchimp', + host: 'smtp.mandrillapp.com:465', user: { value: '', placeholder: 'Your Mailchimp primary contact email' }, - password: { value: '', placeholder: 'Your Mailchimp Transactional API key' }, + auth: { + case: 'plain', + password: { value: '', placeholder: 'Your Mailchimp Transactional API key' }, + }, image: './assets/images/smtp/mailchimp.svg', senderEmailPlaceholder: 'An authorized domain or email address', - routerLinkElement: 'mailchimp', -}; +} as const; -export const BrevoDefaultSettings: ProviderDefaultSettings = { - name: 'brevo', - requiredTls: false, - host: 'smtp-relay.sendinblue.com', - unencryptedPort: 587, - encryptedPort: 465, +mailchimp satisfies ProviderDefaultSettings; + +export const brevo = { + description: 'brevo', + host: 'smtp-relay.sendinblue.com:465', user: { value: '', placeholder: 'Your SMTP login email address' }, - password: { value: '', placeholder: 'Your SMTP key' }, + auth: { + case: 'plain', + password: { value: '', placeholder: 'Your SMTP key' }, + }, image: './assets/images/smtp/brevo.svg', - routerLinkElement: 'brevo', -}; +} as const; -export const OutlookDefaultSettings: ProviderDefaultSettings = { - name: 'outlook.com', - requiredTls: true, - host: 'smtp-mail.outlook.com', - unencryptedPort: 587, - encryptedPort: 587, - user: { value: '', placeholder: 'Your outlook.com email address' }, - password: { value: '', placeholder: 'Your outlook.com password' }, +brevo satisfies ProviderDefaultSettings; + +export const outlook = { + description: 'Microsoft Exchange Online', + host: 'smtp.office365.com:587', + user: { + value: '', + placeholder: 'your outlook.com email address', + }, + auth: { + case: 'xoauth2', + scopes: 'https://outlook.office.com/SMTP.Send', + }, image: './assets/images/smtp/outlook.svg', senderEmailPlaceholder: 'Your outlook.com email address', - routerLinkElement: 'outlook', -}; +} as const; -export const GenericDefaultSettings: ProviderDefaultSettings = { - name: 'generic', - requiredTls: false, - user: { value: '', placeholder: 'your SMTP user' }, - password: { value: '', placeholder: 'your SMTP password' }, - routerLinkElement: 'generic', -}; - -export const SMTPKnownProviders = [ - AmazonSESDefaultSettings, - BrevoDefaultSettings, - // GoogleDefaultSettings, - MailgunDefaultSettings, - MailchimpDefaultSettings, - MailjetDefaultSettings, - PostmarkDefaultSettings, - SendgridDefaultSettings, - OutlookDefaultSettings, - GenericDefaultSettings, -]; +outlook satisfies ProviderDefaultSettings; diff --git a/console/src/app/modules/smtp-provider/smtp-provider-routing.module.ts b/console/src/app/modules/smtp-provider/smtp-provider-routing.module.ts index 3cebddebf..f8442e4bb 100644 --- a/console/src/app/modules/smtp-provider/smtp-provider-routing.module.ts +++ b/console/src/app/modules/smtp-provider/smtp-provider-routing.module.ts @@ -3,35 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; import { SMTPProviderComponent } from './smtp-provider.component'; -const types = [ - { path: 'aws-ses', component: SMTPProviderComponent }, - { path: 'generic', component: SMTPProviderComponent }, - { path: 'google', component: SMTPProviderComponent }, - { path: 'mailgun', component: SMTPProviderComponent }, - { path: 'postmark', component: SMTPProviderComponent }, - { path: 'sendgrid', component: SMTPProviderComponent }, - { path: 'mailjet', component: SMTPProviderComponent }, - { path: 'mailchimp', component: SMTPProviderComponent }, - { path: 'brevo', component: SMTPProviderComponent }, - { path: 'outlook', component: SMTPProviderComponent }, -]; - -const routes: Routes = types.map((value) => { - return { - path: value.path, - children: [ - { - path: 'create', - component: value.component, - }, - ], - }; -}); - -routes.push({ - path: ':id', - component: SMTPProviderComponent, -}); +const routes: Routes = [{ path: ':provider', component: SMTPProviderComponent }]; @NgModule({ imports: [RouterModule.forChild(routes)], diff --git a/console/src/app/modules/smtp-provider/smtp-provider.component.html b/console/src/app/modules/smtp-provider/smtp-provider.component.html index 4f37783f4..87f09c683 100644 --- a/console/src/app/modules/smtp-provider/smtp-provider.component.html +++ b/console/src/app/modules/smtp-provider/smtp-provider.component.html @@ -1,281 +1,345 @@ +@let state = this.state(); +@let stepper = this.stepper();
- -
- -
+ @if (state) { + @if ('defaults' in state) { + + } + @if (!('defaults' in state) && !('config' in state)) { +
+ +
+ } + } +

{{ - !id - ? ('SMTP.CREATE.STEPS.CREATE_DESC_TITLE' | translate: { value: providerDefaultSetting.name | titlecase }) - : ('SMTP.CREATE.STEPS.CURRENT_DESC_TITLE' | translate) + !state || !('defaults' in state) + ? ('SMTP.CREATE.STEPS.CURRENT_DESC_TITLE' | translate) + : ('SMTP.CREATE.STEPS.CREATE_DESC_TITLE' | translate: { value: state.defaults.description | titlecase }) }}

- + @if (configOrDefaultsQuery.isLoading()) { + + } - - - {{ 'SMTP.CREATE.STEPS.PROVIDER_SETTINGS' | translate }} -
- - {{ 'SETTING.SMTP.TLS' | translate }} - + @if (state) { + + + {{ 'SMTP.CREATE.STEPS.PROVIDER_SETTINGS' | translate }} + + + {{ 'SETTING.SMTP.TLS' | translate }} + - - {{ providerDefaultSetting.multiHostsLabel }} - - - {{ region.key }} - - - - - - {{ 'SETTING.SMTP.DESCRIPTION' | translate }} - - - - - {{ 'SETTING.SMTP.HOSTANDPORT' | translate }} - - - - - {{ 'SETTING.SMTP.USER' | translate }} - - - - - {{ 'SETTING.SMTP.PASSWORD' | translate }} - - - - -
- -
-
- - -
- {{ 'SMTP.CREATE.STEPS.SENDER_SETTINGS' | translate }} - - - {{ 'SETTING.SMTP.SENDERADDRESS' | translate }} - - - - - {{ 'SETTING.SMTP.SENDERNAME' | translate }} - - - - - {{ 'SETTING.SMTP.REPLYTOADDRESS' | translate }} - - -
- -
- - -
-
- - -
- {{ 'SMTP.CREATE.STEPS.SAVE_SETTINGS' | translate }} - -
-
-

{{ 'SMTP.CREATE.STEPS.TEST.TITLE' | translate }}

-
-
- -
-
-

{{ 'SMTP.CREATE.STEPS.TEST.DESCRIPTION' | translate }}

- - {{ 'SMTP.LIST.DIALOG.TEST_EMAIL' | translate }} + + {{ 'SETTING.SMTP.DESCRIPTION' | translate }} -
- -
- - - {{ 'SMTP.LIST.DIALOG.TEST_RESULT' | translate }} - + name="hostAndPort" + [formControl]="state.mainForm.controls.host" + placeholder="host:port" + required + /> -
+ + XOAUTH2 + + + {{ 'SETTING.SMTP.USER' | translate }} + + + + @if ('tokenEndpoint' in state.authForm.controls) { + + Token Endpoint + + + + Scopes comma separated + + + + Client ID + + + + Client Secret + + + } @else { + + {{ 'SETTING.SMTP.PASSWORD' | translate }} + + + } +
+ +
+ +
+
+ + +
+ {{ 'SMTP.CREATE.STEPS.SENDER_SETTINGS' | translate }} + + + {{ 'SETTING.SMTP.SENDERADDRESS' | translate }} + + + + + {{ 'SETTING.SMTP.SENDERNAME' | translate }} + + + + + {{ 'SETTING.SMTP.REPLYTOADDRESS' | translate }} + + +
- -
+
- -
+ + + {{ 'SMTP.CREATE.STEPS.SAVE_SETTINGS' | translate }} + +
+
+

{{ 'SMTP.CREATE.STEPS.TEST.TITLE' | translate }}

+
+
+ +
+
+

{{ 'SMTP.CREATE.STEPS.TEST.DESCRIPTION' | translate }}

+ + {{ 'SMTP.LIST.DIALOG.TEST_EMAIL' | translate }} + + + +
+ +
+ + + {{ 'SMTP.LIST.DIALOG.TEST_RESULT' | translate }} + @let error = testEmailConfigurationMutation.error(); + + +
+ +
+ + +
+ +
+ + {{ 'SMTP.CREATE.STEPS.NEXT_STEPS' | translate }} - -
-
-

{{ 'SMTP.CREATE.STEPS.ACTIVATE.TITLE' | translate }}

- -
-
+ @if ('config' in state) { +
+ @if (state.config.state === EmailProviderState.EMAIL_PROVIDER_ACTIVE) { + +
+
+

{{ 'SMTP.CREATE.STEPS.DEACTIVATE.TITLE' | translate }}

+ +
+
+ +
+
+

{{ 'SMTP.CREATE.STEPS.DEACTIVATE.DESCRIPTION' | translate }}

+
+ } @else if (state.config.state === EmailProviderState.EMAIL_PROVIDER_INACTIVE) { + +
+
+

{{ 'SMTP.CREATE.STEPS.ACTIVATE.TITLE' | translate }}

+ +
+
+ +
+
+

{{ 'SMTP.CREATE.STEPS.ACTIVATE.DESCRIPTION' | translate }}

+
+ } + +
+
-
-

{{ 'SMTP.CREATE.STEPS.ACTIVATE.DESCRIPTION' | translate }}

- + + } + - -
-
-

{{ 'SMTP.CREATE.STEPS.DEACTIVATE.TITLE' | translate }}

- -
-
- -
-
-

{{ 'SMTP.CREATE.STEPS.DEACTIVATE.DESCRIPTION' | translate }}

-
- -
- - -
- - - - - check - - + + check + + + } diff --git a/console/src/app/modules/smtp-provider/smtp-provider.component.ts b/console/src/app/modules/smtp-provider/smtp-provider.component.ts index 0a810a30d..2bdd20333 100644 --- a/console/src/app/modules/smtp-provider/smtp-provider.component.ts +++ b/console/src/app/modules/smtp-provider/smtp-provider.component.ts @@ -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 ? NonNullable : 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() ?? 'test@example.com'); - 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 = 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 = 'sender@example.com'; + protected readonly configOrDefaultsQuery: ReturnType; + protected readonly state: ReturnType; + protected readonly updateDataMutation: ReturnType; + protected readonly testEmailConfigurationMutation: ReturnType; - public resultClass = 'test-success'; - public isLoading = signal(false); - public email: string = ''; - public testResult: string = ''; + protected readonly stepper = viewChild(MatStepper); + protected readonly preselectedStep: ReturnType; - 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 || 'sender@example.com'; - - 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 { - 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) { + const mainForm = this.fb.group({ + description: new FormControl(config.description, { + nonNullable: true, + validators: [requiredValidator], + }), + user: new FormControl(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(config.config.value.Auth.case === 'xoauth2', { nonNullable: true }), + }); + + mainForm.controls.xoauth2.disable(); + + const senderForm = this.buildSenderForm(config.config.value); + + return { + mainForm, + senderForm, + senderEmailPlaceholder: 'sender@example.com', + 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(defaults?.description ?? '', { + nonNullable: true, + validators: [requiredValidator], + }), + user: new FormControl(defaults?.user.value ?? '', { nonNullable: true, validators: [requiredValidator] }), + host: new FormControl(defaults?.host ?? '', { + nonNullable: true, + validators: [this.hostnameValidator], + }), + tls: new FormControl(true, { nonNullable: true }), + xoauth2: new FormControl(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 : 'sender@example.com'; + + 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 | ReturnType | 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(defaultValues.tokenEndpoint ?? '', { + nonNullable: true, + validators: [requiredValidator], + }), + scopes: new FormControl(defaultValues.scopes ?? '', { nonNullable: true, validators: [requiredValidator] }), + clientId: new FormControl(defaultValues.clientId ?? '', { + nonNullable: true, + validators: [requiredValidator], + }), + clientSecret: new FormControl('', { 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; + + 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['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['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)); + }, + }; + }); } } diff --git a/console/src/app/modules/smtp-table/smtp-table.component.ts b/console/src/app/modules/smtp-table/smtp-table.component.ts index 4bdac1f75..247223206 100644 --- a/console/src/app/modules/smtp-table/smtp-table.component.ts +++ b/console/src/app/modules/smtp-table/smtp-table.component.ts @@ -193,18 +193,10 @@ export class SMTPTableComponent implements OnInit { this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize); } - public get createRouterLink(): RouterLink | any { - return ['/instance', 'idp', 'create']; - } - public routerLinkForRow(row: SMTPConfig.AsObject): any { return ['/instance', 'smtpprovider', row.id]; } - public get displayedColumnsWithActions(): string[] { - return ['actions', ...this.displayedColumns]; - } - public navigateToProvider(row: SMTPConfig.AsObject) { if (!row.senderAddress) { return; diff --git a/console/src/app/services/new-admin.service.ts b/console/src/app/services/new-admin.service.ts index 665f9f961..ddda9e165 100644 --- a/console/src/app/services/new-admin.service.ts +++ b/console/src/app/services/new-admin.service.ts @@ -2,12 +2,14 @@ import { Injectable } from '@angular/core'; import { GrpcService } from './grpc.service'; import { MessageInitShape } from '@bufbuild/protobuf'; import { + AddEmailProviderSMTPRequestSchema, GetDefaultOrgResponse, GetMyInstanceResponse, SetUpOrgRequestSchema, - SetUpOrgResponse, + TestEmailProviderSMTPRequestSchema, + UpdateEmailProviderSMTPRequestSchema, } from '@zitadel/proto/zitadel/admin_pb'; -import { injectQuery } from '@tanstack/angular-query-experimental'; +import { injectQuery, queryOptions, skipToken } from '@tanstack/angular-query-experimental'; import { NewAuthService } from './new-auth.service'; import { UserService } from './user.service'; @@ -21,7 +23,7 @@ export class NewAdminService { private readonly userService: UserService, ) {} - public setupOrg(req: MessageInitShape): Promise { + public setupOrg(req: MessageInitShape) { return this.grpcService.adminNew.setUpOrg(req); } @@ -41,4 +43,35 @@ export class NewAdminService { enabled: (listMyZitadelPermissionsQuery.data() ?? []).includes('iam.write'), })); } + + public testEmailProviderSMTP(req: MessageInitShape) { + return this.grpcService.adminNew.testEmailProviderSMTP(req); + } + + public getEmailProviderById(id: string, signal: AbortSignal) { + return this.grpcService.adminNew.getEmailProviderById({ id }, { signal }); + } + + public getEmailProviderByIdQueryOptions(id?: string) { + return queryOptions({ + queryKey: [this.userService.userId(), 'AdminService', 'getEmailProviderById', id], + queryFn: id ? ({ signal }) => this.getEmailProviderById(id, signal) : skipToken, + }); + } + + public addEmailProviderSMTP(req: MessageInitShape) { + return this.grpcService.adminNew.addEmailProviderSMTP(req); + } + + public updateEmailProviderSMTP(req: MessageInitShape) { + return this.grpcService.adminNew.updateEmailProviderSMTP(req); + } + + public activateSMTPConfig(id: string) { + return this.grpcService.adminNew.activateSMTPConfig({ id }); + } + + public deactivateSMTPConfig(id: string) { + return this.grpcService.adminNew.deactivateSMTPConfig({ id }); + } } diff --git a/tests/functional-ui/cypress/e2e/instance/settings/notifications.cy.ts b/tests/functional-ui/cypress/e2e/instance/settings/notifications.cy.ts index 99e68a4a0..bed7a966b 100644 --- a/tests/functional-ui/cypress/e2e/instance/settings/notifications.cy.ts +++ b/tests/functional-ui/cypress/e2e/instance/settings/notifications.cy.ts @@ -36,19 +36,19 @@ describe('instance notifications', () => { cy.get('@provider').then((provider) => { cy.visit(smtpPath); cy.get(`a:contains('Mailgun')`).click(); - cy.get('[formcontrolname="description"]').should('be.enabled').clear().type(provider.description); - cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'smtp.mailgun.org:587'); - cy.get('[formcontrolname="user"]').should('be.enabled').clear().type('user@example.com'); - cy.get('[formcontrolname="password"]').should('be.enabled').clear().type('password'); + cy.get('input[name="description"]').should('be.enabled').clear().type(provider.description); + cy.get('input[name="hostAndPort"]').should('have.value', 'smtp.mailgun.org:465'); + cy.get('input[name="smtp-user"]').should('be.enabled').clear().type('user@example.com'); + cy.get('input[name="smtp-password"]').should('be.enabled').clear().type('password'); cy.get('[data-e2e="continue-to-2nd-form"]').should('be.enabled').click(); - cy.get('[formcontrolname="senderAddress"]').should('be.enabled').clear().type('sender1@example.com'); - cy.get('[formcontrolname="senderName"]').should('be.enabled').clear().type('Test1'); - cy.get('[formcontrolname="replyToAddress"]').should('be.enabled').clear().type('replyto1@example.com'); + cy.get('input[name="senderAddress"]').should('be.enabled').clear().type('sender1@example.com'); + cy.get('input[name="senderName"]').should('be.enabled').clear().type('Test1'); + cy.get('input[name="senderReplyToAddress"]').should('be.enabled').clear().type('replyto1@example.com'); cy.get('[data-e2e="continue-button"]').should('be.enabled').click(); cy.get('[data-e2e="create-button"]').should('be.enabled').click(); cy.shouldConfirmSuccess(); cy.get('[data-e2e="close-button"]').should('be.enabled').click(); - cy.get(provider.rowSelector).contains('smtp.mailgun.org:587'); + cy.get(provider.rowSelector).contains('smtp.mailgun.org:465'); cy.get(provider.rowSelector).contains('sender1@example.com'); }); }); @@ -57,14 +57,14 @@ describe('instance notifications', () => { cy.get('@provider').then((provider) => { cy.visit(smtpPath); cy.get(`a:contains('Mailgun')`).click(); - cy.get('[formcontrolname="description"]').should('be.enabled').clear().type(provider.description); - cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'smtp.mailgun.org:587'); - cy.get('[formcontrolname="user"]').should('be.enabled').clear().type('user@example.com'); - cy.get('[formcontrolname="password"]').should('be.enabled').clear().type('password'); + cy.get('input[name="description"]').should('be.enabled').clear().type(provider.description); + cy.get('input[name="hostAndPort"]').should('have.value', 'smtp.mailgun.org:465'); + cy.get('input[name="smtp-user"]').should('be.enabled').clear().type('user@example.com'); + cy.get('input[name="smtp-password"]').should('be.enabled').clear().type('password'); cy.get('[data-e2e="continue-to-2nd-form"]').should('be.enabled').click(); - cy.get('[formcontrolname="senderAddress"]').should('be.enabled').clear().type('sender1@example.com'); - cy.get('[formcontrolname="senderName"]').should('be.enabled').clear().type('Test1'); - cy.get('[formcontrolname="replyToAddress"]').should('be.enabled').clear().type('replyto1@example.com'); + cy.get('input[name="senderAddress"]').should('be.enabled').clear().type('sender1@example.com'); + cy.get('input[name="senderName"]').should('be.enabled').clear().type('Test1'); + cy.get('input[name="senderReplyToAddress"]').should('be.enabled').clear().type('replyto1@example.com'); cy.get('[data-e2e="continue-button"]').should('be.enabled').click(); cy.get('[data-e2e="create-button"]').click(); cy.shouldConfirmSuccess(); @@ -72,7 +72,7 @@ describe('instance notifications', () => { cy.shouldConfirmSuccess(); cy.get('[data-e2e="close-button"]').click(); cy.get(provider.rowSelector).find('[data-e2e="active-provider"]'); - cy.get(provider.rowSelector).contains('smtp.mailgun.org:587'); + cy.get(provider.rowSelector).contains('smtp.mailgun.org:465'); cy.get(provider.rowSelector).contains('sender1@example.com'); }); }); @@ -91,8 +91,8 @@ describe('instance notifications', () => { cy.get('@provider').then(({ rowSelector }) => { cy.get(rowSelector).click(); cy.get('[data-e2e="continue-to-2nd-form"]').click(); - cy.get('[formcontrolname="senderAddress"]').should('be.enabled').clear().type('senderchange1@example.com'); - cy.get('[formcontrolname="senderName"]').clear().type('Change1'); + cy.get('input[name="senderAddress"]').should('be.enabled').clear().type('senderchange1@example.com'); + cy.get('input[name="senderName"]').clear().type('Change1'); cy.get('[data-e2e="continue-button"]').click(); cy.get('[data-e2e="create-button"]').click(); cy.shouldConfirmSuccess(); diff --git a/tests/functional-ui/project.json b/tests/functional-ui/project.json index 542b0e55a..124ce79d0 100644 --- a/tests/functional-ui/project.json +++ b/tests/functional-ui/project.json @@ -1,6 +1,6 @@ { "$schema": "../../node_modules/nx/schemas/project-schema.json", - "implicitDependencies": ["@zitadel/api"], + "implicitDependencies": ["@zitadel/api", "@zitadel/console"], "targets": { "run-db": { "continuous": true,