mirror of
https://github.com/zitadel/zitadel.git
synced 2026-08-17 16:35:14 -05: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:
+22
-26
@@ -2,36 +2,32 @@
|
||||
<p class="cnsl-secondary-text">{{ 'SMTP.LIST.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<div class="cnsl-snmp-table-wrapper">
|
||||
<cnsl-smtp-table></cnsl-smtp-table>
|
||||
<cnsl-smtp-table />
|
||||
</div>
|
||||
|
||||
<h2>{{ 'SMTP.CREATE.TITLE' | translate }}</h2>
|
||||
<p class="cnsl-secondary-text">{{ 'SMTP.CREATE.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<div class="new-smtp-wrapper">
|
||||
<div *ngFor="let provider of providers">
|
||||
<a
|
||||
class="item card"
|
||||
[routerLink]="['/instance', 'smtpprovider', provider.routerLinkElement, 'create']"
|
||||
*ngIf="provider.name !== 'generic'"
|
||||
>
|
||||
<img class="smtp-logo" src="{{ provider.image }}" alt="{{ provider.name }}" />
|
||||
<div class="text-container">
|
||||
<span class="title">{{ provider.name | titlecase }} </span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="item card"
|
||||
[routerLink]="['/instance', 'smtpprovider', provider.routerLinkElement, 'create']"
|
||||
*ngIf="provider.name === 'generic'"
|
||||
>
|
||||
<div class="smtp-icon">
|
||||
<mat-icon class="icon" svgIcon="mdi_smtp" />
|
||||
</div>
|
||||
<div class="text-container">
|
||||
<span class="title">Generic SMTP</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@for (providerEntry of providers | keyvalue; track providerEntry.key) {
|
||||
@let id = providerEntry.key;
|
||||
@let provider = providerEntry.value;
|
||||
<div>
|
||||
<a class="item card" [routerLink]="['/instance', 'smtpprovider', id]">
|
||||
@if (provider.description !== 'generic') {
|
||||
<img class="smtp-logo" [src]="provider.image" [alt]="provider.description" />
|
||||
<div class="text-container">
|
||||
<span class="title">{{ provider.description | titlecase }} </span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="smtp-icon">
|
||||
<mat-icon class="icon" svgIcon="mdi_smtp" />
|
||||
</div>
|
||||
<div class="text-container">
|
||||
<span class="title">Generic SMTP</span>
|
||||
</div>
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
+11
-24
@@ -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<ManagementService>);
|
||||
break;
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
this.service = this.injector.get(AdminService as Type<AdminService>);
|
||||
break;
|
||||
}
|
||||
}
|
||||
export class NotificationSMTPProviderComponent {
|
||||
protected readonly PolicyComponentServiceType = PolicyComponentServiceType;
|
||||
protected readonly providers = { ...SMTPKnownProviders, generic: { description: 'generic' } } as const;
|
||||
}
|
||||
|
||||
-33
@@ -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 {}
|
||||
@@ -27,7 +27,7 @@
|
||||
<cnsl-notification-policy [serviceType]="serviceType"></cnsl-notification-policy>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="setting()?.id === 'smtpprovider' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-notification-smtp-provider [serviceType]="serviceType"></cnsl-notification-smtp-provider>
|
||||
<cnsl-notification-smtp-provider></cnsl-notification-smtp-provider>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="setting()?.id === 'smsprovider' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-notification-sms-provider [serviceType]="serviceType"></cnsl-notification-sms-provider>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -1,281 +1,345 @@
|
||||
@let state = this.state();
|
||||
@let stepper = this.stepper();
|
||||
<cnsl-create-layout
|
||||
title="{{
|
||||
id ? ('SMTP.DETAIL.TITLE' | translate) : ('SMTP.CREATE.STEPS.TITLE' | translate: { value: providerDefaultSetting.name })
|
||||
}}"
|
||||
[title]="
|
||||
!state || 'config' in state
|
||||
? ('SMTP.DETAIL.TITLE' | translate)
|
||||
: ('SMTP.CREATE.STEPS.TITLE' | translate: { value: state.mainForm.controls.description.value })
|
||||
"
|
||||
[createSteps]="4"
|
||||
[currentCreateStep]="currentCreateStep"
|
||||
(closed)="close()"
|
||||
[currentCreateStep]="stepper ? stepper.selectedIndex + 1 : 1"
|
||||
(closed)="location.back()"
|
||||
>
|
||||
<div class="wizard-header">
|
||||
<img
|
||||
class="smtp-logo"
|
||||
src="{{ providerDefaultSetting.image }}"
|
||||
alt="{{ providerDefaultSetting.name }}"
|
||||
*ngIf="providerDefaultSetting.name !== 'generic'"
|
||||
/>
|
||||
<div class="smtp-icon" *ngIf="providerDefaultSetting.name === 'generic'">
|
||||
<mat-icon class="icon" svgIcon="mdi_smtp" alt="providerDefaultSetting.name" />
|
||||
</div>
|
||||
@if (state) {
|
||||
@if ('defaults' in state) {
|
||||
<img class="smtp-logo" [src]="state.defaults.image" [alt]="state.defaults.description" />
|
||||
}
|
||||
@if (!('defaults' in state) && !('config' in state)) {
|
||||
<div class="smtp-icon">
|
||||
<mat-icon class="icon" svgIcon="mdi_smtp" alt="providerDefaultSetting.name" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<h2>
|
||||
{{
|
||||
!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 })
|
||||
}}
|
||||
</h2>
|
||||
</div>
|
||||
<mat-progress-bar class="progress-bar" color="primary" *ngIf="smtpLoading" mode="indeterminate"></mat-progress-bar>
|
||||
@if (configOrDefaultsQuery.isLoading()) {
|
||||
<mat-progress-bar class="progress-bar" color="primary" mode="indeterminate" />
|
||||
}
|
||||
|
||||
<mat-horizontal-stepper
|
||||
class="stepper {{ 'last-edited-step-' + stepper.selectedIndex }}"
|
||||
linear
|
||||
#stepper
|
||||
labelPosition="bottom"
|
||||
(selectionChange)="changeStep($event)"
|
||||
>
|
||||
<mat-step [editable]="true">
|
||||
<ng-template matStepLabel>{{ 'SMTP.CREATE.STEPS.PROVIDER_SETTINGS' | translate }}</ng-template>
|
||||
<form [formGroup]="firstFormGroup" autocomplete="off">
|
||||
<mat-checkbox class="smtp-checkbox" formControlName="tls" (change)="toggleTLS($event)" data-e2e="tls-checkbox">
|
||||
{{ 'SETTING.SMTP.TLS' | translate }}
|
||||
</mat-checkbox>
|
||||
@if (state) {
|
||||
<mat-horizontal-stepper
|
||||
class="stepper {{ 'last-edited-step-' + stepper.selectedIndex }}"
|
||||
[selectedIndex]="preselectedStep()"
|
||||
linear
|
||||
#stepper
|
||||
labelPosition="bottom"
|
||||
>
|
||||
<mat-step [editable]="true">
|
||||
<ng-template matStepLabel>{{ 'SMTP.CREATE.STEPS.PROVIDER_SETTINGS' | translate }}</ng-template>
|
||||
<form [formGroup]="state.mainForm" autocomplete="off">
|
||||
<mat-checkbox class="smtp-checkbox" [formControl]="state.mainForm.controls.tls" data-e2e="tls-checkbox">
|
||||
{{ 'SETTING.SMTP.TLS' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<cnsl-form-field class="smtp-form-field" *ngIf="providerDefaultSetting.regions">
|
||||
<cnsl-label>{{ providerDefaultSetting.multiHostsLabel }}</cnsl-label>
|
||||
<mat-select formControlName="region">
|
||||
<mat-option *ngFor="let region of providerDefaultSetting.regions | keyvalue" [value]="region.value">
|
||||
{{ region.key }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="smtp-form-field" label="Description">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.DESCRIPTION' | translate }}</cnsl-label>
|
||||
<input cnslInput name="description" formControlName="description" placeholder="description" />
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="smtp-form-field" label="Host And Port">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.HOSTANDPORT' | translate }}</cnsl-label>
|
||||
<input cnslInput name="hostAndPort" formControlName="hostAndPort" placeholder="host:port" required />
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="smtp-form-field" label="User">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.USER' | translate }}</cnsl-label>
|
||||
<input
|
||||
id="smtp-user"
|
||||
cnslInput
|
||||
name="smtp-user"
|
||||
autocomplete="smtp-user"
|
||||
formControlName="user"
|
||||
placeholder="{{ providerDefaultSetting.user.placeholder }}"
|
||||
required
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="smtp-form-field" label="Password">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.PASSWORD' | translate }}</cnsl-label>
|
||||
<input
|
||||
id="smtp-password"
|
||||
cnslInput
|
||||
name="smtp-password"
|
||||
autocomplete="off webauthn"
|
||||
formControlName="password"
|
||||
placeholder="{{ hasSMTPConfig ? '****************' : providerDefaultSetting.password.placeholder }}"
|
||||
type="password"
|
||||
required="{{ !hasSMTPConfig }}"
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
|
||||
<div class="smtp-create-actions">
|
||||
<button
|
||||
mat-raised-button
|
||||
[disabled]="firstFormGroup.invalid"
|
||||
color="primary"
|
||||
matStepperNext
|
||||
data-e2e="continue-to-2nd-form"
|
||||
>
|
||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [editable]="true">
|
||||
<form [formGroup]="secondFormGroup">
|
||||
<ng-template matStepLabel>{{ 'SMTP.CREATE.STEPS.SENDER_SETTINGS' | translate }}</ng-template>
|
||||
|
||||
<cnsl-form-field class="smtp-form-field" label="Sender Address">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.SENDERADDRESS' | translate }}</cnsl-label>
|
||||
<input
|
||||
cnslInput
|
||||
name="senderAddress"
|
||||
formControlName="senderAddress"
|
||||
placeholder="{{ senderEmailPlaceholder }}"
|
||||
required
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="smtp-form-field" label="Sender Name">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.SENDERNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput name="senderName" formControlName="senderName" placeholder="Zitadel" required />
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="smtp-form-field" label="Reply-To Address">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.REPLYTOADDRESS' | translate }}</cnsl-label>
|
||||
<input cnslInput name="senderReplyToAddress" formControlName="replyToAddress" placeholder="replyto@example.com" />
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
|
||||
<div class="smtp-create-actions">
|
||||
<button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button>
|
||||
<button
|
||||
mat-raised-button
|
||||
[disabled]="secondFormGroup.invalid"
|
||||
color="primary"
|
||||
matStepperNext
|
||||
data-e2e="continue-button"
|
||||
>
|
||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [editable]="true">
|
||||
<form>
|
||||
<ng-template matStepLabel>{{ 'SMTP.CREATE.STEPS.SAVE_SETTINGS' | translate }}</ng-template>
|
||||
<cnsl-info-section>
|
||||
<div class="title-row">
|
||||
<div class="left">
|
||||
<h2 class="title">{{ 'SMTP.CREATE.STEPS.TEST.TITLE' | translate }}</h2>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button color="primary" mat-raised-button class="continue-button" (click)="testEmailConfiguration()">
|
||||
{{ 'ACTIONS.TEST' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="cnsl-secondary-text description">{{ 'SMTP.CREATE.STEPS.TEST.DESCRIPTION' | translate }}</p>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'SMTP.LIST.DIALOG.TEST_EMAIL' | translate }}</cnsl-label>
|
||||
<cnsl-form-field class="smtp-form-field" label="Description">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.DESCRIPTION' | translate }}</cnsl-label>
|
||||
<input
|
||||
cnslInput
|
||||
[(ngModel)]="email"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
data-e2e="email-test-dialog-input"
|
||||
name="description"
|
||||
[formControl]="state.mainForm.controls.description"
|
||||
placeholder="description"
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
|
||||
<div class="is-loading" *ngIf="isLoading()">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<cnsl-form-field class="formfield" *ngIf="testResult">
|
||||
<cnsl-label>{{ 'SMTP.LIST.DIALOG.TEST_RESULT' | translate }}</cnsl-label>
|
||||
<textarea
|
||||
<cnsl-form-field class="smtp-form-field" label="Host And Port">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.HOSTANDPORT' | translate }}</cnsl-label>
|
||||
<input
|
||||
cnslInput
|
||||
class="{{ resultClass }}"
|
||||
[(ngModel)]="testResult"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
></textarea>
|
||||
name="hostAndPort"
|
||||
[formControl]="state.mainForm.controls.host"
|
||||
placeholder="host:port"
|
||||
required
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
</cnsl-info-section>
|
||||
|
||||
<mat-checkbox class="smtp-checkbox" [formControl]="state.mainForm.controls.xoauth2">XOAUTH2</mat-checkbox>
|
||||
|
||||
<cnsl-form-field class="smtp-form-field" label="User">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.USER' | translate }}</cnsl-label>
|
||||
<input
|
||||
id="smtp-user"
|
||||
cnslInput
|
||||
name="smtp-user"
|
||||
autocomplete="smtp-user"
|
||||
[formControl]="state.mainForm.controls.user"
|
||||
required
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
|
||||
@if ('tokenEndpoint' in state.authForm.controls) {
|
||||
<cnsl-form-field class="smtp-form-field" label="tokenEndpoint">
|
||||
<cnsl-label>Token Endpoint</cnsl-label>
|
||||
<input cnslInput name="token-endpoint" [formControl]="state.authForm.controls.tokenEndpoint" required />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="smtp-form-field" label="scopes">
|
||||
<cnsl-label>Scopes comma separated</cnsl-label>
|
||||
<input cnslInput name="scopes" [formControl]="state.authForm.controls.scopes" required />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="smtp-form-field" label="clientId">
|
||||
<cnsl-label>Client ID</cnsl-label>
|
||||
<input cnslInput name="client-id" [formControl]="state.authForm.controls.clientId" required />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="smtp-form-field" label="clientSecret">
|
||||
<cnsl-label>Client Secret</cnsl-label>
|
||||
<input
|
||||
cnslInput
|
||||
type="password"
|
||||
name="client-id"
|
||||
[formControl]="state.authForm.controls.clientSecret"
|
||||
required
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
} @else {
|
||||
<cnsl-form-field class="smtp-form-field" label="Password">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.PASSWORD' | translate }}</cnsl-label>
|
||||
<input
|
||||
id="smtp-password"
|
||||
cnslInput
|
||||
name="smtp-password"
|
||||
autocomplete="off webauthn"
|
||||
[formControl]="state.authForm.controls.password"
|
||||
[placeholder]="
|
||||
'defaults' in state && state.defaults.auth.case === 'plain' ? state.defaults.auth.password.placeholder : ''
|
||||
"
|
||||
type="password"
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
}
|
||||
</form>
|
||||
|
||||
<div class="smtp-create-actions">
|
||||
<button
|
||||
mat-raised-button
|
||||
[disabled]="state.mainForm.invalid || state.authForm.invalid"
|
||||
color="primary"
|
||||
matStepperNext
|
||||
data-e2e="continue-to-2nd-form"
|
||||
>
|
||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [editable]="true">
|
||||
<form [formGroup]="state.senderForm">
|
||||
<ng-template matStepLabel>{{ 'SMTP.CREATE.STEPS.SENDER_SETTINGS' | translate }}</ng-template>
|
||||
|
||||
<cnsl-form-field class="smtp-form-field" label="Sender Address">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.SENDERADDRESS' | translate }}</cnsl-label>
|
||||
<input
|
||||
cnslInput
|
||||
name="senderAddress"
|
||||
formControlName="senderAddress"
|
||||
[placeholder]="state.senderEmailPlaceholder"
|
||||
required
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="smtp-form-field" label="Sender Name">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.SENDERNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput name="senderName" formControlName="senderName" placeholder="Zitadel" required />
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="smtp-form-field" label="Reply-To Address">
|
||||
<cnsl-label>{{ 'SETTING.SMTP.REPLYTOADDRESS' | translate }}</cnsl-label>
|
||||
<input
|
||||
cnslInput
|
||||
name="senderReplyToAddress"
|
||||
formControlName="replyToAddress"
|
||||
placeholder="replyto@example.com"
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
|
||||
<div class="smtp-create-actions">
|
||||
<button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button>
|
||||
<button
|
||||
mat-raised-button
|
||||
class="create-button"
|
||||
[disabled]="state.senderForm.invalid"
|
||||
color="primary"
|
||||
data-e2e="create-button"
|
||||
(click)="savePolicy(stepper)"
|
||||
[disabled]="
|
||||
firstFormGroup.invalid || secondFormGroup.invalid || (['iam.policy.write'] | hasRole | async) === false
|
||||
"
|
||||
matStepperNext
|
||||
data-e2e="continue-button"
|
||||
>
|
||||
{{ !hasSMTPConfig ? ('ACTIONS.CREATE' | translate) : ('ACTIONS.SAVE' | translate) }}
|
||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [editable]="true">
|
||||
<form>
|
||||
<mat-step [editable]="true" [completed]="'config' in state">
|
||||
<form>
|
||||
<ng-template matStepLabel>{{ 'SMTP.CREATE.STEPS.SAVE_SETTINGS' | translate }}</ng-template>
|
||||
<cnsl-info-section>
|
||||
<div class="title-row">
|
||||
<div class="left">
|
||||
<h2 class="title">{{ 'SMTP.CREATE.STEPS.TEST.TITLE' | translate }}</h2>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button
|
||||
color="primary"
|
||||
mat-raised-button
|
||||
class="continue-button"
|
||||
(click)="testEmailConfigurationMutation.mutate()"
|
||||
>
|
||||
{{ 'ACTIONS.TEST' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="cnsl-secondary-text description">{{ 'SMTP.CREATE.STEPS.TEST.DESCRIPTION' | translate }}</p>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'SMTP.LIST.DIALOG.TEST_EMAIL' | translate }}</cnsl-label>
|
||||
<input
|
||||
[disabled]="emailQuery.isLoading()"
|
||||
cnslInput
|
||||
(ngModelChange)="email.set($event)"
|
||||
[ngModel]="email()"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
data-e2e="email-test-dialog-input"
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
|
||||
<div class="is-loading" *ngIf="testEmailConfigurationMutation.isPending()">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<cnsl-form-field
|
||||
class="formfield"
|
||||
*ngIf="testEmailConfigurationMutation.isError() || testEmailConfigurationMutation.isSuccess()"
|
||||
>
|
||||
<cnsl-label>{{ 'SMTP.LIST.DIALOG.TEST_RESULT' | translate }}</cnsl-label>
|
||||
@let error = testEmailConfigurationMutation.error();
|
||||
<textarea
|
||||
cnslInput
|
||||
[ngModel]="error ? error.message : ('SMTP.CREATE.STEPS.TEST.RESULT' | translate)"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
></textarea>
|
||||
</cnsl-form-field>
|
||||
</cnsl-info-section>
|
||||
|
||||
<div class="smtp-create-actions">
|
||||
<button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button>
|
||||
<button
|
||||
(click)="updateDataMutation.mutate()"
|
||||
mat-raised-button
|
||||
class="create-button"
|
||||
color="primary"
|
||||
data-e2e="create-button"
|
||||
[disabled]="
|
||||
state.mainForm.invalid || state.senderForm.invalid || (['iam.policy.write'] | hasRole | async) === false
|
||||
"
|
||||
>
|
||||
{{ 'config' in state ? ('ACTIONS.SAVE' | translate) : ('ACTIONS.CREATE' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [editable]="true">
|
||||
<ng-template matStepLabel>{{ 'SMTP.CREATE.STEPS.NEXT_STEPS' | translate }}</ng-template>
|
||||
<cnsl-info-section *ngIf="!isActive">
|
||||
<div class="title-row">
|
||||
<div class="left">
|
||||
<h2 class="title">{{ 'SMTP.CREATE.STEPS.ACTIVATE.TITLE' | translate }}</h2>
|
||||
<div>
|
||||
<a
|
||||
mat-icon-button
|
||||
card-actions
|
||||
mat-icon-button
|
||||
href="https://zitadel.com/docs/guides/manage/console/default-settings#smtp"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<mat-icon class="next-icon">info_outline</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
@if ('config' in state) {
|
||||
<form>
|
||||
@if (state.config.state === EmailProviderState.EMAIL_PROVIDER_ACTIVE) {
|
||||
<cnsl-info-section>
|
||||
<div class="title-row">
|
||||
<div class="left">
|
||||
<h2 class="title">{{ 'SMTP.CREATE.STEPS.DEACTIVATE.TITLE' | translate }}</h2>
|
||||
<div>
|
||||
<a
|
||||
mat-icon-button
|
||||
card-actions
|
||||
mat-icon-button
|
||||
href="https://zitadel.com/docs/guides/manage/console/default-settings#smtp"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<mat-icon class="next-icon">info_outline</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button
|
||||
color="primary"
|
||||
mat-raised-button
|
||||
class="continue-button"
|
||||
data-e2e="deactivate-button"
|
||||
(click)="deactivateSMTPConfig(state.config.id); $event.stopPropagation()"
|
||||
>
|
||||
{{ 'ACTIONS.DEACTIVATE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="cnsl-secondary-text description">{{ 'SMTP.CREATE.STEPS.DEACTIVATE.DESCRIPTION' | translate }}</p>
|
||||
</cnsl-info-section>
|
||||
} @else if (state.config.state === EmailProviderState.EMAIL_PROVIDER_INACTIVE) {
|
||||
<cnsl-info-section>
|
||||
<div class="title-row">
|
||||
<div class="left">
|
||||
<h2 class="title">{{ 'SMTP.CREATE.STEPS.ACTIVATE.TITLE' | translate }}</h2>
|
||||
<div>
|
||||
<a
|
||||
mat-icon-button
|
||||
card-actions
|
||||
mat-icon-button
|
||||
href="https://zitadel.com/docs/guides/manage/console/default-settings#smtp"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<mat-icon class="next-icon">info_outline</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button
|
||||
color="primary"
|
||||
mat-raised-button
|
||||
class="continue-button"
|
||||
data-e2e="activate-button"
|
||||
(click)="activateSMTPConfig(state.config.id); $event.stopPropagation()"
|
||||
>
|
||||
{{ 'ACTIONS.ACTIVATE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="cnsl-secondary-text description">{{ 'SMTP.CREATE.STEPS.ACTIVATE.DESCRIPTION' | translate }}</p>
|
||||
</cnsl-info-section>
|
||||
}
|
||||
|
||||
<div class="smtp-create-actions">
|
||||
<button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button>
|
||||
<button
|
||||
color="primary"
|
||||
mat-raised-button
|
||||
class="continue-button"
|
||||
data-e2e="activate-button"
|
||||
(click)="activateSMTPConfig(); $event.stopPropagation()"
|
||||
class="create-button"
|
||||
color="primary"
|
||||
data-e2e="close-button"
|
||||
(click)="location.back()"
|
||||
>
|
||||
{{ 'ACTIONS.ACTIVATE' | translate }}
|
||||
{{ 'ACTIONS.CLOSE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="cnsl-secondary-text description">{{ 'SMTP.CREATE.STEPS.ACTIVATE.DESCRIPTION' | translate }}</p>
|
||||
</cnsl-info-section>
|
||||
</form>
|
||||
}
|
||||
</mat-step>
|
||||
|
||||
<cnsl-info-section *ngIf="isActive">
|
||||
<div class="title-row">
|
||||
<div class="left">
|
||||
<h2 class="title">{{ 'SMTP.CREATE.STEPS.DEACTIVATE.TITLE' | translate }}</h2>
|
||||
<div>
|
||||
<a
|
||||
mat-icon-button
|
||||
card-actions
|
||||
mat-icon-button
|
||||
href="https://zitadel.com/docs/guides/manage/console/default-settings#smtp"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<mat-icon class="next-icon">info_outline</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button
|
||||
color="primary"
|
||||
mat-raised-button
|
||||
class="continue-button"
|
||||
data-e2e="deactivate-button"
|
||||
(click)="deactivateSMTPConfig(); $event.stopPropagation()"
|
||||
>
|
||||
{{ 'ACTIONS.DEACTIVATE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="cnsl-secondary-text description">{{ 'SMTP.CREATE.STEPS.DEACTIVATE.DESCRIPTION' | translate }}</p>
|
||||
</cnsl-info-section>
|
||||
|
||||
<div class="smtp-create-actions">
|
||||
<button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button>
|
||||
<button mat-raised-button class="create-button" color="primary" data-e2e="close-button" (click)="close()">
|
||||
{{ 'ACTIONS.CLOSE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<ng-template matStepperIcon="edit">
|
||||
<mat-icon>check</mat-icon>
|
||||
</ng-template>
|
||||
</mat-horizontal-stepper>
|
||||
<ng-template matStepperIcon="edit">
|
||||
<mat-icon>check</mat-icon>
|
||||
</ng-template>
|
||||
</mat-horizontal-stepper>
|
||||
}
|
||||
</cnsl-create-layout>
|
||||
|
||||
@@ -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() ?? '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<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 = 'sender@example.com';
|
||||
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 || '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<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: '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<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 : '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<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));
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<typeof SetUpOrgRequestSchema>): Promise<SetUpOrgResponse> {
|
||||
public setupOrg(req: MessageInitShape<typeof SetUpOrgRequestSchema>) {
|
||||
return this.grpcService.adminNew.setUpOrg(req);
|
||||
}
|
||||
|
||||
@@ -41,4 +43,35 @@ export class NewAdminService {
|
||||
enabled: (listMyZitadelPermissionsQuery.data() ?? []).includes('iam.write'),
|
||||
}));
|
||||
}
|
||||
|
||||
public testEmailProviderSMTP(req: MessageInitShape<typeof TestEmailProviderSMTPRequestSchema>) {
|
||||
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<typeof AddEmailProviderSMTPRequestSchema>) {
|
||||
return this.grpcService.adminNew.addEmailProviderSMTP(req);
|
||||
}
|
||||
|
||||
public updateEmailProviderSMTP(req: MessageInitShape<typeof UpdateEmailProviderSMTPRequestSchema>) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,19 +36,19 @@ describe('instance notifications', () => {
|
||||
cy.get<SMTPProvider>('@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<SMTPProvider>('@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<SMTPProvider>('@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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user