feat: SMTP Templates (#6932)

* feat: smtp templates poc

* feat: add isActive & ProviderType to SMTP backend

* feat: change providertype to uint32 and fix tests

* feat: minimal smtp provider component

* feat: woking on diiferent providers

* feat: keep working on providers

* feat: initial stepper for new provider

* fix: settings list and working on stepper

* feat: step 1 and 2 form inputs

* feat: starter for smtp test step

* fix: misspelled SMPT

* fix: remove tests for now

* feat: add tls toggle remove old google provider

* feat: working on add smtp and table

* fix: duplicated identifiers

* fix: settings list

* fix: add missing smtp config properties

* fix: add configID to smtp config table

* fix: working on listproviders

* feat: working in listSMTPConfigs

* fix: add count to listsmtpconfigs

* fix: getting empty results from listSMTPConfigs

* feat: table now shows real data

* fix: remaining styles for smtp-table

* fix: remove old notification-smtp-provider-component

* feat: delete smtp configuration

* feat: deactivate smtp config

* feat: replace isActive with state for smtp config

* feat: activate smtp config

* fix: remaining errors after main merge

* fix: list smtp providers panic and material mdc

* feat: refactor to only one provider component

* feat: current provider details view

* fix: refactor AddSMTPConfig and ChangeSMTPConfig

* fix: smtp config reduce issue

* fix: recover domain in NewIAMSMTPConfigWriteModel

* fix: add code needed by SetUpInstance

* fix: go tests and warn about passing context to InstanceAggregateFromWriteModel

* fix: i18n and add missing trans for fr, it, zh

* fix: add e2e tests

* docs: add smtp templates

* fix: remove provider_type, add description

* fix: remaining error from merge main

* fix: add @stebenz change for primary key

* fix: inactive placed after removed to prevent deleted configs to show as inactive

* fix: smtp provider id can be empty (migrated)

* feat: add mailchimp transactional template

* feat: add Brevo (Sendinblue) template

* feat: change brevo logo, add color to tls icon

* fix: queries use resourceowner, id must not be empty

* fix: deal with old smtp settings and tests

* fix: resourceOwner is the instanceID

* fix: remove aggregate_id, rename SMTPConfigByAggregateID with SMTPConfigActive

* fix: add tests for multiple configs with different IDs

* fix: conflict

* fix: remove notification-smtp-provider

* fix: add @peintnermax suggestions, rename module and fix e2e tests

* fix: remove material legacy modules

* fix: remove ctx as parameter for  InstanceAggregateFromWriteModel

* fix: add Id to SMTPConfigToPb

* fix:  change InstanceAggregateFromWriteModel to avoid linter errors

* fix import

* rm unused package-lock

* update yarn lock

---------

Co-authored-by: Elio Bischof <[email protected]>
Co-authored-by: Max Peintner <[email protected]>
Co-authored-by: Stefan Benz <[email protected]>
This commit is contained in:
Miguel Cabrerizo
2024-04-11 09:16:10 +02:00
committed by GitHub
co-authored by Elio Bischof Max Peintner Stefan Benz
parent e2f0cd034a
commit d229da6af7
93 changed files with 6359 additions and 6132 deletions
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider.component';
describe('PasswordDialogComponent', () => {
let component: DialogAddSMSProviderComponent;
let fixture: ComponentFixture<DialogAddSMSProviderComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [DialogAddSMSProviderComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DialogAddSMSProviderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -10,7 +10,7 @@ import {
import { SMSProvider, TwilioConfig } from 'src/app/proto/generated/zitadel/settings_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
import { PasswordDialogComponent } from '../password-dialog/password-dialog.component';
import { PasswordDialogSMSProviderComponent } from '../password-dialog-sms-provider/password-dialog-sms-provider.component';
enum SMSProviderType {
Twilio = 1,
@@ -73,7 +73,7 @@ export class DialogAddSMSProviderComponent {
}
public changeToken(): void {
const dialogRef = this.dialog.open(PasswordDialogComponent, {
const dialogRef = this.dialog.open(PasswordDialogSMSProviderComponent, {
width: '400px',
data: {
i18nTitle: 'SETTING.SMS.TWILIO.SETTOKEN',
@@ -6,6 +6,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { TranslateModule } from '@ngx-translate/core';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
@@ -16,10 +17,11 @@ import { InputModule } from '../../input/input.module';
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { NotificationSMSProviderComponent } from './notification-sms-provider.component';
import { PasswordDialogSMSProviderComponent } from './password-dialog-sms-provider/password-dialog-sms-provider.component';
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [NotificationSMSProviderComponent, DialogAddSMSProviderComponent],
declarations: [NotificationSMSProviderComponent, DialogAddSMSProviderComponent, PasswordDialogSMSProviderComponent],
imports: [
CommonModule,
CardModule,
@@ -34,9 +36,9 @@ import { MatDialogModule } from '@angular/material/dialog';
FormFieldModule,
WarnDialogModule,
MatSelectModule,
MatDialogModule,
MatProgressSpinnerModule,
MatSelectModule,
MatDialogModule,
TranslateModule,
],
exports: [NotificationSMSProviderComponent],
@@ -1,6 +1,6 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PasswordDialogComponent } from './password-dialog.component';
import { PasswordDialogComponent } from './password-dialog-sms-provider.component';
describe('PasswordDialogComponent', () => {
let component: PasswordDialogComponent;
@@ -2,14 +2,14 @@ import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
@Component({
selector: 'cnsl-password-dialog',
templateUrl: './password-dialog.component.html',
styleUrls: ['./password-dialog.component.scss'],
selector: 'cnsl-password-dialog-sms-provider',
templateUrl: './password-dialog-sms-provider.component.html',
styleUrls: ['./password-dialog-sms-provider.component.scss'],
})
export class PasswordDialogComponent {
export class PasswordDialogSMSProviderComponent {
public password: string = '';
constructor(
public dialogRef: MatDialogRef<PasswordDialogComponent>,
public dialogRef: MatDialogRef<PasswordDialogSMSProviderComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) {}
@@ -1,70 +1,37 @@
<h2>{{ 'DESCRIPTIONS.SETTINGS.SMTP_PROVIDER.TITLE' | translate }}</h2>
<p class="cnsl-secondary-text">{{ 'DESCRIPTIONS.SETTINGS.SMTP_PROVIDER.DESCRIPTION' | translate }}</p>
<h2>{{ 'SMTP.LIST.TITLE' | translate }}</h2>
<p class="cnsl-secondary-text">{{ 'SMTP.LIST.DESCRIPTION' | translate }}</p>
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="smtpLoading" color="primary"></mat-spinner>
<div class="cnsl-snmp-table-wrapper">
<cnsl-smtp-table></cnsl-smtp-table>
</div>
<cnsl-info-section
*ngIf="!smtpLoading && !form.valid"
class="info-section-warn"
[fitWidth]="true"
[type]="InfoSectionType.ALERT"
>{{ 'SETTING.SMTP.REQUIREDWARN' | translate }}</cnsl-info-section
>
<h2>{{ 'SMTP.CREATE.TITLE' | translate }}</h2>
<p class="cnsl-secondary-text">{{ 'SMTP.CREATE.DESCRIPTION' | translate }}</p>
<form (ngSubmit)="savePolicy()" [formGroup]="form" autocomplete="off">
<cnsl-form-field class="smtp-form-field" label="Sender Address" required="true">
<cnsl-label>{{ 'SETTING.SMTP.SENDERADDRESS' | translate }}</cnsl-label>
<input cnslInput name="senderAddress" formControlName="senderAddress" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Sender Name" required="true">
<cnsl-label>{{ 'SETTING.SMTP.SENDERNAME' | translate }}</cnsl-label>
<input cnslInput name="senderName" formControlName="senderName" 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" />
</cnsl-form-field>
<mat-checkbox class="smtp-checkbox" formControlName="tls">
{{ 'SETTING.SMTP.TLS' | translate }}
</mat-checkbox>
<cnsl-form-field class="smtp-form-field" label="Host And Port" required="true">
<cnsl-label>{{ 'SETTING.SMTP.HOSTANDPORT' | translate }}</cnsl-label>
<input cnslInput name="hostAndPort" formControlName="hostAndPort" placeholder="smtp.mailtrap.io:2525" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="User" required="true">
<cnsl-label>{{ 'SETTING.SMTP.USER' | translate }}</cnsl-label>
<input id="smtp-user" cnslInput name="smtp-user" autocomplete="smtp-user" formControlName="user" required />
</cnsl-form-field>
<button
class="set-password-btn"
[disabled]="(['iam.write'] | hasRole | async) === false || !hasSMTPConfig"
(click)="setSMTPPassword()"
type="button"
mat-stroked-button
data-e2e="add-smtp-password-button"
>
{{ 'SETTING.SMTP.SETPASSWORD' | translate }}
</button>
<div class="general-btn-container">
<button
class="save-button"
[disabled]="form.disabled"
(click)="savePolicy()"
color="primary"
type="submit"
mat-raised-button
data-e2e="save-smtp-settings-button"
<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'"
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
<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>
</form>
</div>
@@ -1,32 +1,113 @@
.spinner-wr {
margin: 0.5rem 0;
}
@use '@angular/material' as mat;
.smtp-form-field,
.info-section-warn {
max-width: 400px;
display: block;
}
@mixin smtp-settings-theme($theme) {
$primary: map-get($theme, primary);
$primary-color: mat.get-color-from-palette($primary, 500);
$is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.info-section-warn {
margin-bottom: 0.5rem;
}
.smtp-checkbox {
max-width: 400px;
display: block;
margin: 1rem 0;
}
.set-password-btn {
margin-bottom: 1rem;
}
.general-btn-container {
display: flex;
justify-content: flex-start;
.save-button {
.cnsl-smtp-table-wrapper {
display: block;
}
.new-smtp-wrapper {
display: grid;
row-gap: 1.5rem;
column-gap: 1.5rem;
box-sizing: border-box;
width: 100%;
grid-template-columns: 1fr;
@media only screen and (min-width: 700px) {
grid-template-columns: 1fr 1fr;
}
@media only screen and (min-width: 1000px) {
grid-template-columns: 1fr 1fr 1fr;
}
@media only screen and (min-width: 1300px) {
grid-template-columns: 1fr 1fr 1fr 1fr;
}
.item {
position: relative;
z-index: 100;
display: flex;
text-decoration: none;
cursor: pointer;
padding-top: 1rem;
padding-right: 1rem;
padding-bottom: 1rem;
padding-left: 1rem;
border-radius: 0.5rem;
box-sizing: border-box;
transition: box-shadow 0.1s ease-in;
align-items: center;
color: map-get($foreground, text);
.coming-soon-label {
position: absolute;
top: 0;
right: 1rem;
transform: translateY(-50%);
width: fit-content;
}
&:hover {
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
}
.smtp-logo {
margin-right: 1rem;
height: 36px;
width: 36px;
&.apple {
margin-bottom: 4px;
}
&.dark {
display: if($is-dark-theme, block, none);
}
&.light {
display: if($is-dark-theme, none, block);
}
}
.smtp-icon {
margin-right: 1rem;
display: flex;
justify-content: center;
align-items: center;
height: 36px;
width: 36px;
.icon {
font-size: 2.25rem;
height: 2.25rem;
width: 2.25rem;
}
}
.text-container {
display: flex;
flex-direction: column;
.title {
}
}
&.coming-soon {
filter: grayscale(1);
cursor: not-allowed;
&:hover {
box-shadow: none;
}
}
}
}
}
@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotificationSMTPProviderComponent } from './notification-smtp-provider.component';
describe('NotificationSMTPProviderComponent', () => {
describe('IdpSettingsComponent', () => {
let component: NotificationSMTPProviderComponent;
let fixture: ComponentFixture<NotificationSMTPProviderComponent>;
@@ -1,22 +1,9 @@
import { Component, Input, OnInit } from '@angular/core';
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { take } from 'rxjs';
import {
AddSMTPConfigRequest,
AddSMTPConfigResponse,
UpdateSMTPConfigPasswordRequest,
UpdateSMTPConfigRequest,
UpdateSMTPConfigResponse,
} from 'src/app/proto/generated/zitadel/admin_pb';
import { Component, Injector, Input, OnInit, Type } from '@angular/core';
import { AdminService } from 'src/app/services/admin.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { ManagementService } from 'src/app/services/mgmt.service';
import { InfoSectionType } from '../../info-section/info-section.component';
import { PasswordDialogComponent } from '../notification-sms-provider/password-dialog/password-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { SMTPKnownProviders } from '../../smtp-provider/known-smtp-providers-settings';
@Component({
selector: 'cnsl-notification-smtp-provider',
@@ -25,151 +12,21 @@ import { PolicyComponentServiceType } from '../policy-component-types.enum';
})
export class NotificationSMTPProviderComponent implements OnInit {
@Input() public serviceType!: PolicyComponentServiceType;
public service!: ManagementService | AdminService;
public smtpLoading: boolean = false;
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public providers = SMTPKnownProviders;
public form!: UntypedFormGroup;
public InfoSectionType: any = InfoSectionType;
public hasSMTPConfig: boolean = false;
// show available providers
constructor(
private service: AdminService,
private dialog: MatDialog,
private toast: ToastService,
private fb: UntypedFormBuilder,
private authService: GrpcAuthService,
) {
this.form = this.fb.group({
senderAddress: [{ disabled: true, value: '' }, [requiredValidator]],
senderName: [{ disabled: true, value: '' }, [requiredValidator]],
replyToAddress: [{ disabled: true, value: '' }],
tls: [{ disabled: true, value: true }, [requiredValidator]],
hostAndPort: [{ disabled: true, value: '' }, [requiredValidator]],
user: [{ disabled: true, value: '' }, [requiredValidator]],
});
}
constructor(private injector: Injector) {}
ngOnInit(): void {
this.fetchData();
this.authService
.isAllowed(['iam.write'])
.pipe(take(1))
.subscribe((allowed) => {
if (allowed) {
this.form.enable();
}
});
}
private fetchData(): void {
this.smtpLoading = true;
this.service
.getSMTPConfig()
.then((smtpConfig) => {
this.smtpLoading = false;
if (smtpConfig.smtpConfig) {
this.hasSMTPConfig = true;
this.form.patchValue(smtpConfig.smtpConfig);
this.form.patchValue({ ['hostAndPort']: smtpConfig.smtpConfig.host });
}
})
.catch((error) => {
this.smtpLoading = false;
if (error && error.code === 5) {
console.log(error);
this.hasSMTPConfig = false;
}
});
}
private updateData(): Promise<UpdateSMTPConfigResponse.AsObject | AddSMTPConfigResponse> {
if (this.hasSMTPConfig) {
const req = new UpdateSMTPConfigRequest();
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 ?? '');
return this.service.updateSMTPConfig(req);
} else {
const req = new AddSMTPConfigRequest();
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 ?? '');
return this.service.addSMTPConfig(req);
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;
}
}
public savePolicy(): void {
this.updateData()
.then(() => {
this.toast.showInfo('SETTING.SMTP.SAVED', true);
setTimeout(() => {
this.fetchData();
}, 2000);
})
.catch((error: unknown) => {
this.toast.showError(error);
});
}
public setSMTPPassword(): void {
const dialogRef = this.dialog.open(PasswordDialogComponent, {
width: '400px',
data: {
i18nTitle: 'SETTING.SMTP.SETPASSWORD',
i18nLabel: 'SETTING.SMTP.PASSWORD',
},
});
dialogRef.afterClosed().subscribe((password: string) => {
if (password) {
const passwordReq = new UpdateSMTPConfigPasswordRequest();
passwordReq.setPassword(password);
this.service
.updateSMTPConfigPassword(passwordReq)
.then(() => {
this.toast.showInfo('SETTING.SMTP.PASSWORDSET', true);
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
public get senderAddress(): AbstractControl | null {
return this.form.get('senderAddress');
}
public get senderName(): AbstractControl | null {
return this.form.get('senderName');
}
public get replyToAddress(): AbstractControl | null {
return this.form.get('replyToAddress');
}
public get tls(): AbstractControl | null {
return this.form.get('tls');
}
public get user(): AbstractControl | null {
return this.form.get('user');
}
public get hostAndPort(): AbstractControl | null {
return this.form.get('hostAndPort');
}
}
@@ -1,43 +1,32 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
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 { FormFieldModule } from '../../form-field/form-field.module';
import { InfoSectionModule } from '../../info-section/info-section.module';
import { InputModule } from '../../input/input.module';
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
import { PasswordDialogComponent } from '../notification-sms-provider/password-dialog/password-dialog.component';
import { NotificationSMTPProviderComponent } from './notification-smtp-provider.component';
import { MatDialogModule } from '@angular/material/dialog';
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, PasswordDialogComponent],
declarations: [NotificationSMTPProviderComponent],
imports: [
CommonModule,
CardModule,
InfoSectionModule,
FormsModule,
ReactiveFormsModule,
HasRolePipeModule,
MatButtonModule,
MatCheckboxModule,
InputModule,
MatIconModule,
FormFieldModule,
WarnDialogModule,
MatSelectModule,
CommonModule,
MatButtonModule,
CardModule,
MatIconModule,
SMTPTableModule,
RouterModule,
HasRolePipeModule,
MatProgressSpinnerModule,
MatSelectModule,
TranslateModule,
MatDialogModule,
],
exports: [NotificationSMTPProviderComponent],
})
@@ -14,7 +14,6 @@ import { LoginTextsPolicyModule } from '../policies/login-texts/login-texts.modu
import { MessageTextsPolicyModule } from '../policies/message-texts/message-texts.module';
import { NotificationPolicyModule } from '../policies/notification-policy/notification-policy.module';
import { NotificationSMSProviderModule } from '../policies/notification-sms-provider/notification-sms-provider.module';
import { NotificationSMTPProviderModule } from '../policies/notification-smtp-provider/notification-smtp-provider.module';
import { OIDCConfigurationModule } from '../policies/oidc-configuration/oidc-configuration.module';
import { PasswordComplexityPolicyModule } from '../policies/password-complexity-policy/password-complexity-policy.module';
import { PasswordLockoutPolicyModule } from '../policies/password-lockout-policy/password-lockout-policy.module';
@@ -28,7 +27,9 @@ 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';
@NgModule({
declarations: [SettingsListComponent],
@@ -44,12 +45,14 @@ import { FeaturesComponent } from 'src/app/components/features/features.componen
LanguageSettingsModule,
NotificationPolicyModule,
IdpSettingsModule,
NotificationSMTPProviderModule,
PrivacyPolicyModule,
MessageTextsPolicyModule,
SecurityPolicyModule,
DomainsModule,
LoginTextsPolicyModule,
OrgTableModule,
OrgListModule,
DomainPolicyModule,
TranslateModule,
HasRolePipeModule,
@@ -0,0 +1,174 @@
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;
user: {
value: string;
placeholder: string;
};
password: {
value: string;
placeholder: string;
};
image?: string;
routerLinkElement: string;
}
export const AmazonSESDefaultSettings: ProviderDefaultSettings = {
name: 'amazon SES',
regions: amazonEndpoints,
multiHostsLabel: 'Choose your region',
requiredTls: true,
encryptedPort: 587,
user: { value: '', placeholder: 'your Amazon SES credentials for this region' },
password: { value: '', placeholder: 'your Amazon SES credentials for this region' },
image: './assets/images/smtp/aws-ses.svg',
routerLinkElement: 'aws-ses',
};
export const GoogleDefaultSettings: ProviderDefaultSettings = {
name: 'google',
requiredTls: true,
host: 'smtp.gmail.com',
unencryptedPort: 587,
encryptedPort: 587,
user: { value: '', placeholder: 'your complete Google Workspace email address' },
password: { value: '', placeholder: 'your complete Google Workspace password' },
image: './assets/images/smtp/google.png',
routerLinkElement: 'google',
};
export const MailgunDefaultSettings: ProviderDefaultSettings = {
name: 'mailgun',
requiredTls: false,
host: 'smtp.mailgun.org',
unencryptedPort: 587,
encryptedPort: 465,
user: { value: '', placeholder: 'postmaster@YOURDOMAIN' },
password: { value: '', placeholder: 'Your mailgun smtp password' },
image: './assets/images/smtp/mailgun.svg',
routerLinkElement: 'mailgun',
};
export const MailjetDefaultSettings: ProviderDefaultSettings = {
name: 'mailjet',
requiredTls: false,
host: 'in-v3.mailjet.com',
unencryptedPort: 587,
encryptedPort: 465,
user: { value: '', placeholder: 'Your Mailjet API key' },
password: { value: '', placeholder: 'Your Mailjet Secret key' },
image: './assets/images/smtp/mailjet.svg',
routerLinkElement: 'mailjet',
};
export const PostmarkDefaultSettings: ProviderDefaultSettings = {
name: 'postmark',
requiredTls: false,
host: 'smtp.postmarkapp.com',
unencryptedPort: 587,
encryptedPort: 587,
user: { value: '', placeholder: 'Your Server API token' },
password: { value: '', placeholder: 'Your Server API token' },
image: './assets/images/smtp/postmark.png',
routerLinkElement: 'postmark',
};
export const SendgridDefaultSettings: ProviderDefaultSettings = {
name: 'sendgrid',
requiredTls: false,
host: 'smtp.sendgrid.net',
unencryptedPort: 587,
encryptedPort: 465,
user: { value: 'apikey', placeholder: '' },
password: { value: '', placeholder: ' Your SendGrid API Key' },
image: './assets/images/smtp/sendgrid.png',
routerLinkElement: 'sendgrid',
};
export const MailchimpDefaultSettings: ProviderDefaultSettings = {
name: 'mailchimp',
requiredTls: false,
host: 'smtp.mandrillapp.com',
unencryptedPort: 587,
encryptedPort: 465,
user: { value: '', placeholder: 'Your Mailchimp primary contact email' },
password: { value: '', placeholder: 'Your Mailchimp Transactional API key' },
image: './assets/images/smtp/mailchimp.svg',
routerLinkElement: 'mailchimp',
};
export const BrevoDefaultSettings: ProviderDefaultSettings = {
name: 'brevo',
requiredTls: false,
host: 'smtp-relay.sendinblue.com',
unencryptedPort: 587,
encryptedPort: 465,
user: { value: '', placeholder: 'Your SMTP login email address' },
password: { value: '', placeholder: 'Your SMTP key' },
image: './assets/images/smtp/brevo.svg',
routerLinkElement: 'brevo',
};
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,
GenericDefaultSettings,
];
@@ -0,0 +1,39 @@
import { NgModule } from '@angular/core';
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 },
];
const routes: Routes = types.map((value) => {
return {
path: value.path,
children: [
{
path: 'create',
component: value.component,
},
],
};
});
routes.push({
path: ':id',
component: SMTPProviderComponent,
});
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class SMTPProvidersRoutingModule {}
@@ -0,0 +1,144 @@
<cnsl-create-layout
title="{{
id ? ('SMTP.DETAIL.TITLE' | translate) : ('SMTP.CREATE.STEPS.TITLE' | translate: { value: providerDefaultSetting.name })
}}"
[createSteps]="2"
[currentCreateStep]="currentCreateStep"
(closed)="close()"
>
<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>
<h2>
{{
!id
? ('SMTP.CREATE.STEPS.CREATE_DESC_TITLE' | translate: { value: providerDefaultSetting.name | titlecase })
: ('SMTP.CREATE.STEPS.CURRENT_DESC_TITLE' | translate)
}}
</h2>
</div>
<mat-progress-bar class="progress-bar" color="primary" *ngIf="smtpLoading" mode="indeterminate"></mat-progress-bar>
<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>
<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-button"
>
{{ '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="[email protected]" 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="[email protected]" />
</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"
color="primary"
data-e2e="create-button"
(click)="savePolicy()"
[disabled]="
firstFormGroup.invalid || secondFormGroup.invalid || (['iam.policy.write'] | hasRole | async) === false
"
>
{{ !hasSMTPConfig ? ('ACTIONS.CREATE' | translate) : ('ACTIONS.SAVE' | translate) }}
</button>
</div>
</mat-step>
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
</mat-horizontal-stepper>
</cnsl-create-layout>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SMTPProviderComponent } from './smtp-provider.component';
describe('SMTPProviderSendgridComponent', () => {
let component: SMTPProviderComponent;
let fixture: ComponentFixture<SMTPProviderComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SMTPProviderComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SMTPProviderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,278 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Subject } from 'rxjs';
import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { Options } from 'src/app/proto/generated/zitadel/idp_pb';
import { requiredValidator } from '../form-field/validators/validators';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
import {
AddSMTPConfigRequest,
AddSMTPConfigResponse,
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,
SendgridDefaultSettings,
} from './known-smtp-providers-settings';
@Component({
selector: 'cnsl-smtp-provider',
templateUrl: './smtp-provider.component.html',
styleUrls: ['./smtp-provider.scss'],
})
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;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
public smtpLoading: boolean = false;
public hasSMTPConfig: boolean = false;
public updateClientSecret: boolean = false;
// stepper
public currentCreateStep: number = 1;
public requestRedirectValuesSubject$: Subject<void> = new Subject();
public firstFormGroup!: UntypedFormGroup;
public secondFormGroup!: UntypedFormGroup;
constructor(
private service: AdminService,
private _location: Location,
private fb: UntypedFormBuilder,
private toast: ToastService,
private router: Router,
private route: ActivatedRoute,
) {
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;
}
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.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);
}
}
});
}
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.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,
});
}
})
.catch((error) => {
this.smtpLoading = false;
if (error && error.code === 5) {
this.hasSMTPConfig = false;
}
});
}
private updateData(): Promise<UpdateSMTPConfigResponse.AsObject | AddSMTPConfigResponse> {
if (this.hasSMTPConfig) {
const req = new UpdateSMTPConfigRequest();
req.setId(this.id);
req.setDescription(this.description?.value || '');
req.setTls(this.tls?.value ?? false);
if (this.hostAndPort && this.hostAndPort.value) {
req.setHost(this.hostAndPort.value);
}
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);
}
}
public savePolicy(): void {
this.updateData()
.then(() => {
this.toast.showInfo('SETTING.SMTP.SAVED', true);
setTimeout(() => {
this.close();
}, 2000);
})
.catch((error: unknown) => {
if (`${error}`.includes('No changes')) {
this.toast.showInfo('SETTING.SMTP.NOCHANGES', true);
setTimeout(() => {
this.close();
}, 2000);
} else {
this.toast.showError(error);
}
});
}
public get description(): AbstractControl | null {
return this.firstFormGroup.get('description');
}
public get tls(): AbstractControl | null {
return this.firstFormGroup.get('tls');
}
public get region(): AbstractControl | null {
return this.firstFormGroup.get('region');
}
public get hostAndPort(): AbstractControl | null {
return this.firstFormGroup.get('hostAndPort');
}
public get user(): AbstractControl | null {
return this.firstFormGroup.get('user');
}
public get password(): AbstractControl | null {
return this.firstFormGroup.get('password');
}
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');
}
}
@@ -0,0 +1,51 @@
import { CommonModule, TitleCasePipe } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
import { InputModule } from 'src/app/modules/input/input.module';
import { MatStepperModule } from '@angular/material/stepper';
import { CardModule } from '../card/card.module';
import { CreateLayoutModule } from '../create-layout/create-layout.module';
import { InfoSectionModule } from '../info-section/info-section.module';
import { ProviderOptionsModule } from '../provider-options/provider-options.module';
import { StringListModule } from '../string-list/string-list.module';
import { SMTPProvidersRoutingModule } from './smtp-provider-routing.module';
import { SMTPProviderComponent } from './smtp-provider.component';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatChipsModule } from '@angular/material/chips';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@NgModule({
declarations: [SMTPProviderComponent],
imports: [
SMTPProvidersRoutingModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
CreateLayoutModule,
StringListModule,
InfoSectionModule,
InputModule,
MatButtonModule,
MatProgressBarModule,
MatSelectModule,
MatIconModule,
MatChipsModule,
CardModule,
MatCheckboxModule,
MatTooltipModule,
MatStepperModule,
TranslateModule,
ProviderOptionsModule,
HasRolePipeModule,
MatProgressSpinnerModule,
],
})
export default class SMTPProviderModule {}
@@ -0,0 +1,71 @@
@use '@angular/material' as mat;
@mixin smtp-provider-theme($theme) {
$is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.stepper {
background: inherit !important;
margin: 0 -1.5rem;
}
.smtp-create-actions {
margin-top: 2rem;
display: flex;
align-items: center;
justify-content: space-between;
.bck-button {
margin-right: 1rem;
}
.create-button {
padding: 0.5rem 4rem;
}
}
.smtp-form-field,
.info-section-warn {
max-width: 400px;
display: block;
}
.wizard-header {
display: flex;
align-items: center;
.smtp-logo {
margin-right: 1rem;
height: 48px;
width: 48px;
flex-shrink: 0;
}
.smtp-icon {
margin-right: 1rem;
display: flex;
justify-content: center;
align-items: center;
height: 36px;
width: 36px;
.icon {
font-size: 2.25rem;
height: 2.25rem;
width: 2.25rem;
}
}
}
.row {
display: flex;
justify-content: space-between;
.left,
.right {
margin-bottom: 0.5rem;
font-size: 14px;
}
}
}
@@ -0,0 +1,129 @@
<cnsl-refresh-table
[loading]="loading$ | async"
(refreshed)="refreshPage()"
[dataSize]="dataSource.data.length"
[emitRefreshOnPreviousRoutes]="[
'/instance/smtpprovider/aws-ses/create',
'/instance/smtpprovider/generic/create',
'/instance/smtpprovider/google/create',
'/instance/smtpprovider/mailgun/create',
'/instance/smtpprovider/mailjet/create',
'/instance/smtpprovider/postmark/create',
'/instance/smtpprovider/sendgrid/create',
'/instance/smtpprovider/mailchimp/create',
'/instance/smtpprovider/brevo/create'
]"
[timestamp]="configsResult?.details?.viewTimestamp"
[selection]="selection"
[hideRefresh]="true"
>
<div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="activated">
<th class="availability" mat-header-cell *matHeaderCellDef>
<span>{{ 'SMTP.LIST.ACTIVATED' | translate }}</span>
</th>
<td class="pointer availability" mat-cell *matCellDef="let config">
<i
matTooltip="{{ 'SMTP.LIST.ACTIVATED' | translate }}"
*ngIf="isActive(config.state)"
class="smtp-available las la-check-circle"
data-e2e="active-provider"
></i>
</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>{{ 'SETTING.SMTP.DESCRIPTION' | translate }}</th>
<td class="pointer" mat-cell *matCellDef="let config">
<span>{{ config?.description }}</span>
</td>
</ng-container>
<ng-container matColumnDef="tls">
<th class="availability" mat-header-cell *matHeaderCellDef>
<span>TLS</span>
</th>
<td class="pointer availability" mat-cell *matCellDef="let config">
<i *ngIf="config.tls" class="las la-lock"></i>
<i *ngIf="!config.tls" class="las la-unlock"></i>
</td>
</ng-container>
<ng-container matColumnDef="host">
<th mat-header-cell *matHeaderCellDef>{{ 'SETTING.SMTP.HOSTANDPORT' | translate }}</th>
<td class="pointer" mat-cell *matCellDef="let config">
<span>{{ config?.host }}</span>
</td>
</ng-container>
<ng-container matColumnDef="senderAddress">
<th mat-header-cell *matHeaderCellDef>{{ 'SETTING.SMTP.SENDERADDRESS' | translate }}</th>
<td class="pointer" mat-cell *matCellDef="let config">
<span>{{ config?.senderAddress }}</span>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th class="smtp-table-actions" mat-header-cell *matHeaderCellDef></th>
<td class="smtp-table-actions" mat-cell *matCellDef="let config">
<cnsl-table-actions>
<button
actions
*ngIf="!isActive(config.state)"
[disabled]="(['iam.write'] | hasRole | async) === false"
mat-icon-button
matTooltip="{{ 'SMTP.LIST.ACTIVATE' | translate }}"
data-e2e="activate-provider-button"
(click)="activateSMTPConfig(config.id); $event.stopPropagation()"
>
<i class="las la-check-circle"></i>
</button>
<button
actions
*ngIf="isActive(config.state)"
[disabled]="(['iam.write'] | hasRole | async) === false"
mat-icon-button
matTooltip="{{ 'SMTP.LIST.DEACTIVATE' | translate }}"
data-e2e="deactivate-provider-button"
(click)="deactivateSMTPConfig(config.id); $event.stopPropagation()"
>
<i class="las la-times-circle"></i>
</button>
<button
actions
[disabled]="(['iam.write'] | hasRole | async) === false"
mat-icon-button
color="warn"
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
data-e2e="delete-provider-button"
(click)="deleteSMTPConfig(config.id, config.senderName); $event.stopPropagation()"
>
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" (click)="navigateToProvider(row)" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'SMTP.LIST.EMPTY' | translate }}</span>
</div>
<cnsl-paginator
#paginator
class="paginator"
[timestamp]="configsResult?.details?.viewTimestamp"
[length]="configsResult?.details?.totalResult || 0"
[pageSize]="10"
[pageSizeOptions]="[10, 20, 50, 100]"
(page)="changePage($event)"
></cnsl-paginator>
</cnsl-refresh-table>
@@ -0,0 +1,81 @@
@mixin smtp-table-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary-color: map-get($primary, 500);
$warn-color: map-get($warn, 500);
$accent-color: map-get($accent, 500);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
.smtp-table-provider-type {
display: flex;
align-items: center;
.smtp-logo {
margin-right: 1rem;
height: 28px;
width: 28px;
flex-shrink: 0;
&.dark {
display: if($is-dark-theme, block, none);
}
&.light {
display: if($is-dark-theme, none, block);
}
}
.smtp-icon {
font-size: 1.75rem;
height: 1.75rem;
width: 1.75rem;
margin-right: 1rem;
flex-shrink: 0;
}
}
}
.smtp-margin-right {
margin-right: 0.5rem;
}
td {
outline: none;
}
tr {
outline: none;
&.disabled,
&.disabled * {
opacity: 0.8;
cursor: default;
}
}
.availability {
width: 40px;
}
.smtp-available {
color: var(--success);
}
.smtp-not-available {
color: var(--warn);
}
.smtp-table-actions {
width: 100px;
}
.la-unlock {
color: orangered;
}
.la-lock {
color: darkorange;
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IdpTableComponent } from './smtp-table.component';
describe('UserTableComponent', () => {
let component: IdpTableComponent;
let fixture: ComponentFixture<IdpTableComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [IdpTableComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IdpTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,194 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { SMTPConfigState } from 'src/app/proto/generated/zitadel/settings_pb';
import { ListQuery } from 'src/app/proto/generated/zitadel/object_pb';
import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
import { PageEvent, PaginatorComponent } from '../paginator/paginator.component';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
import { ListSMTPConfigsRequest, ListSMTPConfigsResponse } from 'src/app/proto/generated/zitadel/admin_pb';
import { SMTPConfig } from 'src/app/proto/generated/zitadel/settings_pb';
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'cnsl-smtp-table',
templateUrl: './smtp-table.component.html',
styleUrls: ['./smtp-table.component.scss'],
})
export class SMTPTableComponent implements OnInit {
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
public dataSource: MatTableDataSource<SMTPConfig.AsObject> = new MatTableDataSource<SMTPConfig.AsObject>();
public selection: SelectionModel<SMTPConfig.AsObject> = new SelectionModel<SMTPConfig.AsObject>(true, []);
public configsResult?: ListSMTPConfigsResponse.AsObject;
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public displayedColumns: string[] = ['activated', 'description', 'tls', 'host', 'senderAddress', 'actions'];
@Output() public changedSelection: EventEmitter<Array<SMTPConfig.AsObject>> = new EventEmitter();
public loginPolicy!: LoginPolicy.AsObject;
constructor(
private adminService: AdminService,
public translate: TranslateService,
private toast: ToastService,
private dialog: MatDialog,
private router: Router,
) {
this.selection.changed.subscribe(() => {
this.changedSelection.emit(this.selection.selected);
});
}
ngOnInit(): void {
this.getData(10, 0);
}
public isActive(state: number) {
return state === SMTPConfigState.SMTP_CONFIG_ACTIVE;
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ? this.selection.clear() : this.dataSource.data.forEach((row) => this.selection.select(row));
}
public changePage(event: PageEvent): void {
this.getData(event.pageSize, event.pageIndex * event.pageSize);
}
public activateSMTPConfig(id: string): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.CONTINUE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'SMTP.LIST.DIALOG.ACTIVATE_WARN_TITLE',
descriptionKey: 'SMTP.LIST.DIALOG.ACTIVATE_WARN_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.adminService
.activateSMTPConfig(id)
.then(() => {
this.toast.showInfo('SMTP.LIST.DIALOG.ACTIVATED', true);
this.refreshPage();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
public deactivateSMTPConfig(id: string): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.CONTINUE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'SMTP.LIST.DIALOG.DEACTIVATE_WARN_TITLE',
descriptionKey: 'SMTP.LIST.DIALOG.DEACTIVATE_WARN_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.adminService
.deactivateSMTPConfig(id)
.then(() => {
this.toast.showInfo('SMTP.LIST.DIALOG.DEACTIVATED', true);
this.refreshPage();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
public deleteSMTPConfig(id: string, senderName: string): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'SMTP.LIST.DIALOG.DELETE_TITLE',
descriptionKey: 'SMTP.LIST.DIALOG.DELETE_DESCRIPTION',
confirmationKey: 'SMTP.LIST.DIALOG.SENDER',
confirmation: senderName,
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.adminService
.removeSMTPConfig(id)
.then(() => {
this.toast.showInfo('SMTP.LIST.DIALOG.DELETED', true);
this.refreshPage();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
private async getData(limit: number, offset: number): Promise<void> {
this.loadingSubject.next(true);
const req = new ListSMTPConfigsRequest();
const lq = new ListQuery();
lq.setOffset(offset);
lq.setLimit(limit);
req.setQuery(lq);
this.adminService
.listSMTPConfigs()
.then((resp) => {
this.configsResult = resp;
if (resp.resultList) {
this.dataSource.data = resp.resultList;
}
this.loadingSubject.next(false);
})
.catch((error) => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
public refreshPage(): void {
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) {
this.router.navigate(this.routerLinkForRow(row));
}
}
@@ -0,0 +1,46 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { TruncatePipeModule } from 'src/app/pipes/truncate-pipe/truncate-pipe.module';
import { PaginatorModule } from '../paginator/paginator.module';
import { TableActionsModule } from '../table-actions/table-actions.module';
import { SMTPTableComponent } from './smtp-table.component';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatTableModule } from '@angular/material/table';
@NgModule({
declarations: [SMTPTableComponent],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatButtonModule,
TableActionsModule,
MatCheckboxModule,
MatIconModule,
MatTooltipModule,
TranslateModule,
LocalizedDatePipeModule,
TimestampToDatePipeModule,
MatTableModule,
PaginatorModule,
RouterModule,
RefreshTableModule,
HasRoleModule,
HasRolePipeModule,
TruncatePipeModule,
],
exports: [SMTPTableComponent],
})
export class SMTPTableModule {}