mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-14 01:45:12 +00:00
feat: send test mail (#7968)
# Which Problems Are Solved - Zitadel doesn't have a way to test SMTP settings either before creating a new provider or once the SMTP provider has been created. - Zitadel SMTP messages can be more informative for usual errors # How the Problems Are Solved - A new step is added to the new/update SMTP provider wizard that allows us to test a configuration. The result is shown in a text area. - From the table of SMTP providers you can test your settings too. - The email address to send the email is by default the email address for the logged in user as suggested. - Some of the SMTP error messages have been changed to give more information about the possible situation. For example: could not contact with the SMTP server, check the port, firewall issues... instead of could not dial Here's a video showing this new option in action: https://github.com/zitadel/zitadel/assets/30386061/50128ba1-c9fa-4481-8eec-e79a3ca69bda # Additional Changes Replace this example text with a concise list of additional changes that this PR introduces, that are not directly solving the initial problem but are related. For example: - The docs explicitly describe that the property XY is mandatory - Adds missing translations for validations. # Additional Context - Closes #4504
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
title="{{
|
||||
id ? ('SMTP.DETAIL.TITLE' | translate) : ('SMTP.CREATE.STEPS.TITLE' | translate: { value: providerDefaultSetting.name })
|
||||
}}"
|
||||
[createSteps]="3"
|
||||
[createSteps]="4"
|
||||
[currentCreateStep]="currentCreateStep"
|
||||
(closed)="close()"
|
||||
>
|
||||
@@ -93,7 +93,7 @@
|
||||
[disabled]="firstFormGroup.invalid"
|
||||
color="primary"
|
||||
matStepperNext
|
||||
data-e2e="continue-button"
|
||||
data-e2e="continue-to-2nd-form"
|
||||
>
|
||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||
</button>
|
||||
@@ -130,19 +130,74 @@
|
||||
<button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button>
|
||||
<button
|
||||
mat-raised-button
|
||||
class="create-button"
|
||||
[disabled]="secondFormGroup.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>
|
||||
</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>
|
||||
<input
|
||||
cnslInput
|
||||
[(ngModel)]="email"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
data-e2e="email-test-dialog-input"
|
||||
/>
|
||||
</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
|
||||
cnslInput
|
||||
class="{{ resultClass }}"
|
||||
[(ngModel)]="testResult"
|
||||
[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
|
||||
mat-raised-button
|
||||
class="create-button"
|
||||
color="primary"
|
||||
data-e2e="create-button"
|
||||
(click)="savePolicy(stepper)"
|
||||
[disabled]="
|
||||
firstFormGroup.invalid || secondFormGroup.invalid || (['iam.policy.write'] | hasRole | async) === false
|
||||
"
|
||||
>
|
||||
{{ !hasSMTPConfig ? ('ACTIONS.CREATE' | translate) : ('ACTIONS.SAVE' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [editable]="true">
|
||||
<form>
|
||||
<ng-template matStepLabel>{{ 'SMTP.CREATE.STEPS.NEXT_STEPS' | translate }}</ng-template>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Subject, take } 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';
|
||||
@@ -11,6 +11,7 @@ import { PolicyComponentServiceType } from '../policies/policy-component-types.e
|
||||
import {
|
||||
AddSMTPConfigRequest,
|
||||
AddSMTPConfigResponse,
|
||||
TestSMTPConfigRequest,
|
||||
UpdateSMTPConfigRequest,
|
||||
UpdateSMTPConfigResponse,
|
||||
} from 'src/app/proto/generated/zitadel/admin_pb';
|
||||
@@ -31,8 +32,10 @@ import {
|
||||
OutlookDefaultSettings,
|
||||
SendgridDefaultSettings,
|
||||
} from './known-smtp-providers-settings';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { MatStepper } from '@angular/material/stepper';
|
||||
import { SMTPConfigState } from 'src/app/proto/generated/zitadel/settings_pb';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-smtp-provider',
|
||||
@@ -61,6 +64,11 @@ export class SMTPProviderComponent {
|
||||
|
||||
public senderEmailPlaceholder = '[email protected]';
|
||||
|
||||
public resultClass = 'test-success';
|
||||
public isLoading = signal(false);
|
||||
public email: string = '';
|
||||
public testResult: string = '';
|
||||
|
||||
constructor(
|
||||
private service: AdminService,
|
||||
private _location: Location,
|
||||
@@ -68,6 +76,8 @@ export class SMTPProviderComponent {
|
||||
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;
|
||||
@@ -136,6 +146,17 @@ export class SMTPProviderComponent {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -281,6 +302,38 @@ export class SMTPProviderComponent {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
public get description(): AbstractControl | null {
|
||||
return this.firstFormGroup.get('description');
|
||||
}
|
||||
|
||||
@@ -96,4 +96,26 @@
|
||||
width: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.continue-button {
|
||||
margin-top: 3rem;
|
||||
display: block;
|
||||
height: 3.5rem;
|
||||
padding: 0 4rem;
|
||||
}
|
||||
|
||||
.is-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.test-error {
|
||||
border-color: #ff3b5b !important;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.test-success {
|
||||
border-color: #10b981 !important;
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,18 @@
|
||||
<i class="las la-times-circle"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
actions
|
||||
[disabled]="(['iam.write'] | hasRole | async) === false"
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
matTooltip="{{ 'SMTP.LIST.TEST' | translate }}"
|
||||
data-e2e="test-provider-button"
|
||||
(click)="testSMTPConfig(config.id); $event.stopPropagation()"
|
||||
>
|
||||
<i class="las la-flask"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
actions
|
||||
[disabled]="(['iam.write'] | hasRole | async) === false"
|
||||
|
||||
@@ -16,6 +16,7 @@ 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';
|
||||
import { SmtpTestDialogComponent } from '../smtp-test-dialog/smtp-test-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-smtp-table',
|
||||
@@ -149,6 +150,21 @@ export class SMTPTableComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public testSMTPConfig(id: string): void {
|
||||
this.dialog.open(SmtpTestDialogComponent, {
|
||||
data: {
|
||||
id: id,
|
||||
confirmKey: 'ACTIONS.TEST',
|
||||
cancelKey: 'ACTIONS.CLOSE',
|
||||
titleKey: 'SMTP.LIST.DIALOG.TEST_TITLE',
|
||||
descriptionKey: 'SMTP.LIST.DIALOG.TEST_DESCRIPTION',
|
||||
emailKey: 'SMTP.LIST.DIALOG.TEST_EMAIL',
|
||||
testResultKey: 'SMTP.LIST.DIALOG.TEST_RESULT',
|
||||
},
|
||||
width: '500px',
|
||||
});
|
||||
}
|
||||
|
||||
private async getData(limit: number, offset: number): Promise<void> {
|
||||
this.loadingSubject.next(true);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { SmtpTestDialogModule } from '../smtp-test-dialog/smtp-test-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [SMTPTableComponent],
|
||||
@@ -40,6 +41,7 @@ import { MatTableModule } from '@angular/material/table';
|
||||
HasRoleModule,
|
||||
HasRolePipeModule,
|
||||
TruncatePipeModule,
|
||||
SmtpTestDialogModule,
|
||||
],
|
||||
exports: [SMTPTableComponent],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<h1 mat-dialog-title>
|
||||
<span>{{ data.titleKey | translate: data.titleParam }}</span>
|
||||
</h1>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<div class="icon-wrapper" *ngIf="data.icon">
|
||||
<i class="icon {{ data.icon }}"></i>
|
||||
</div>
|
||||
<p class="desc cnsl-secondary-text">{{ data.descriptionKey | translate: data.descriptionParam }}</p>
|
||||
|
||||
<cnsl-info-section *ngIf="data.warnSectionKey" [type]="InfoSectionType.WARN">
|
||||
{{ data.warnSectionKey | translate }}
|
||||
</cnsl-info-section>
|
||||
|
||||
<p *ngIf="data.hintKey" class="desc cnsl-secondary-text">{{ data.hintKey | translate: { value: data.confirmation } }}</p>
|
||||
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ data.emailKey | translate }}</cnsl-label>
|
||||
<input cnslInput [(ngModel)]="email" data-e2e="email-test-dialog-input" />
|
||||
</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>{{ data.testResultKey | translate }}</cnsl-label>
|
||||
<textarea cnslInput class="{{ resultClass }}" [(ngModel)]="testResult"></textarea>
|
||||
</cnsl-form-field>
|
||||
</div>
|
||||
|
||||
<div class="action" mat-dialog-actions>
|
||||
<button *ngIf="data.cancelKey" mat-stroked-button (click)="closeDialog()">
|
||||
{{ data.cancelKey | translate }}
|
||||
</button>
|
||||
<span class="fill-space"></span>
|
||||
<button
|
||||
color="primary"
|
||||
[disabled]="!email || isLoading()"
|
||||
mat-raised-button
|
||||
class="ok-button"
|
||||
(click)="testEmailConfiguration()"
|
||||
data-e2e="test-email-button"
|
||||
>
|
||||
{{ data.confirmKey | translate }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
margin: 1rem;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.ok-button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.is-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.test-error {
|
||||
border-color: #ff3b5b;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.test-success {
|
||||
border-color: #10b981;
|
||||
min-height: 200px;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { SmtpTestDialogComponent } from './smtp-test-dialog.component';
|
||||
|
||||
describe('SmtpTestDialogComponent', () => {
|
||||
let component: SmtpTestDialogComponent;
|
||||
let fixture: ComponentFixture<SmtpTestDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SmtpTestDialogComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SmtpTestDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Component, Inject, signal } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
import { InfoSectionType } from '../info-section/info-section.component';
|
||||
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 { TranslateService } from '@ngx-translate/core';
|
||||
import { TestSMTPConfigByIdRequest } from 'src/app/proto/generated/zitadel/admin_pb';
|
||||
import { take } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-smtp-test-dialog',
|
||||
templateUrl: './smtp-test-dialog.component.html',
|
||||
styleUrls: ['./smtp-test-dialog.component.scss'],
|
||||
})
|
||||
export class SmtpTestDialogComponent {
|
||||
public resultClass = 'test-success';
|
||||
public isLoading = signal(false);
|
||||
public email: string = '';
|
||||
public testResult: string = '';
|
||||
InfoSectionType: any = InfoSectionType;
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<SmtpTestDialogComponent>,
|
||||
private adminService: AdminService,
|
||||
private authService: GrpcAuthService,
|
||||
private toast: ToastService,
|
||||
private translate: TranslateService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
) {
|
||||
this.authService
|
||||
.getMyUser()
|
||||
.then((resp) => {
|
||||
if (resp.user) {
|
||||
this.email = resp.user.human?.email?.email || '';
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public testEmailConfiguration(): void {
|
||||
this.isLoading.set(true);
|
||||
|
||||
const req = new TestSMTPConfigByIdRequest();
|
||||
req.setId(this.data.id);
|
||||
req.setReceiverAddress(this.email);
|
||||
|
||||
this.adminService
|
||||
.testSMTPConfigById(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;
|
||||
});
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { InfoSectionModule } from '../info-section/info-section.module';
|
||||
import { InputModule } from '../input/input.module';
|
||||
import { SmtpTestDialogComponent } from './smtp-test-dialog.component';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
|
||||
@NgModule({
|
||||
declarations: [SmtpTestDialogComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatProgressSpinnerModule,
|
||||
TranslateModule,
|
||||
InfoSectionModule,
|
||||
MatButtonModule,
|
||||
InputModule,
|
||||
],
|
||||
})
|
||||
export class SmtpTestDialogModule {}
|
||||
Reference in New Issue
Block a user