feat(console): set email verified on change, user create (#2847)

* feat: set email verified on change

* user create
This commit is contained in:
Max Peintner 2021-12-15 11:23:53 +01:00 committed by GitHub
parent a533872c66
commit fb43b13232
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 289 additions and 251 deletions

View File

@ -7,17 +7,17 @@
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}*</cnsl-label> <cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="userName" required /> <input cnslInput formControlName="userName" required />
<span cnsl-error *ngIf="userName?.invalid && userName?.errors?.required"> <span cnslError *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}
</span> </span>
<span cnsl-error *ngIf="userName?.invalid && userName?.errors?.noEmailValidator"> <span cnslError *ngIf="userName?.invalid && userName?.errors?.noEmailValidator">
{{ 'USER.VALIDATION.NOEMAIL' | translate }} {{ 'USER.VALIDATION.NOEMAIL' | translate }}
</span> </span>
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}*</cnsl-label> <cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="name" required /> <input cnslInput formControlName="name" required />
<span cnsl-error *ngIf="name?.invalid && name?.errors?.required"> <span cnslError *ngIf="name?.invalid && name?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}
</span> </span>
</cnsl-form-field> </cnsl-form-field>

View File

@ -52,6 +52,15 @@
</span> </span>
</cnsl-form-field> </cnsl-form-field>
<div class="email-is-verified">
<mat-checkbox class="verified-checkbox" formControlName="isVerified">
{{'USER.LOGINMETHODS.EMAIL.ISVERIFIED' | translate}}
</mat-checkbox>
<cnsl-info-section class="full-width desc">
<span>{{'USER.LOGINMETHODS.EMAIL.ISVERIFIEDDESC' | translate}}</span>
</cnsl-info-section>
</div>
<p class="section">{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}</p> <p class="section">{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}</p>
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">

View File

@ -1,4 +1,3 @@
.form { .form {
width: 100%; width: 100%;
padding-top: 1rem; padding-top: 1rem;
@ -7,8 +6,8 @@
button { button {
margin-top: 3rem; margin-top: 3rem;
display: block; display: block;
padding: .5rem 4rem; padding: 0.5rem 4rem;
border-radius: .5rem; border-radius: 0.5rem;
} }
} }
} }
@ -18,19 +17,25 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
flex-direction: row; flex-direction: row;
margin: 0 -.5rem; margin: 0 -0.5rem;
.section { .section {
padding: .5rem; padding: 0.5rem;
flex-basis: 100%; flex-basis: 100%;
color: var(--grey); color: var(--grey);
font-size: .9rem; font-size: 0.9rem;
letter-spacing: .05em; letter-spacing: 0.05em;
text-transform: uppercase; text-transform: uppercase;
} }
.formfield { .formfield {
flex: 1 0 33%; flex: 1 0 33%;
margin: 0 .5rem; margin: 0 0.5rem;
}
.email-is-verified {
margin: 0 0.5rem;
flex-basis: 100%;
margin-top: 1.5rem;
} }
} }

View File

@ -38,7 +38,9 @@ export class UserCreateComponent implements OnDestroy {
) { ) {
this.loading = true; this.loading = true;
this.loadOrg(); this.loadOrg();
this.mgmtService.getOrgIAMPolicy().then((resp) => { this.mgmtService
.getOrgIAMPolicy()
.then((resp) => {
if (resp.policy?.userLoginMustBeDomain) { if (resp.policy?.userLoginMustBeDomain) {
this.userLoginMustBeDomain = resp.policy.userLoginMustBeDomain; this.userLoginMustBeDomain = resp.policy.userLoginMustBeDomain;
} }
@ -46,7 +48,8 @@ export class UserCreateComponent implements OnDestroy {
this.loading = false; this.loading = false;
this.envSuffixLabel = this.envSuffix(); this.envSuffixLabel = this.envSuffix();
this.changeDetRef.detectChanges(); this.changeDetRef.detectChanges();
}).catch(error => { })
.catch((error) => {
console.error(error); console.error(error);
this.initForm(); this.initForm();
this.loading = false; this.loading = false;
@ -56,8 +59,8 @@ export class UserCreateComponent implements OnDestroy {
} }
private async loadOrg(): Promise<void> { private async loadOrg(): Promise<void> {
const domains = (await this.mgmtService.listOrgDomains()); const domains = await this.mgmtService.listOrgDomains();
const found = domains.resultList.find(resp => resp.isPrimary); const found = domains.resultList.find((resp) => resp.isPrimary);
if (found) { if (found) {
this.primaryDomain = found; this.primaryDomain = found;
} }
@ -66,23 +69,17 @@ export class UserCreateComponent implements OnDestroy {
private initForm(): void { private initForm(): void {
this.userForm = this.fb.group({ this.userForm = this.fb.group({
email: ['', [Validators.required, Validators.email]], email: ['', [Validators.required, Validators.email]],
userName: ['', userName: ['', [Validators.required, Validators.minLength(2)]],
[
Validators.required,
Validators.minLength(2),
],
],
firstName: ['', Validators.required], firstName: ['', Validators.required],
lastName: ['', Validators.required], lastName: ['', Validators.required],
nickName: [''], nickName: [''],
gender: [], gender: [],
preferredLanguage: [''], preferredLanguage: [''],
phone: [''], phone: [''],
isVerified: [false, []],
}); });
this.userForm.controls['phone'].valueChanges.pipe( this.userForm.controls['phone'].valueChanges.pipe(takeUntil(this.destroyed$), debounceTime(300)).subscribe((value) => {
takeUntil(this.destroyed$),
debounceTime(300)).subscribe(value => {
const phoneNumber = parsePhoneNumber(value ?? '', 'CH'); const phoneNumber = parsePhoneNumber(value ?? '', 'CH');
if (phoneNumber) { if (phoneNumber) {
const formmatted = phoneNumber.formatInternational(); const formmatted = phoneNumber.formatInternational();
@ -110,7 +107,10 @@ export class UserCreateComponent implements OnDestroy {
humanReq.setUserName(this.userName?.value); humanReq.setUserName(this.userName?.value);
humanReq.setProfile(profileReq); humanReq.setProfile(profileReq);
humanReq.setEmail(new AddHumanUserRequest.Email().setEmail(this.email?.value)); const emailreq = new AddHumanUserRequest.Email();
emailreq.setEmail(this.email?.value);
emailreq.setIsEmailVerified(this.isVerified?.value);
humanReq.setEmail(emailreq);
if (this.phone && this.phone.value) { if (this.phone && this.phone.value) {
humanReq.setPhone(new AddHumanUserRequest.Phone().setPhone(this.phone.value)); humanReq.setPhone(new AddHumanUserRequest.Phone().setPhone(this.phone.value));
@ -123,7 +123,7 @@ export class UserCreateComponent implements OnDestroy {
this.toast.showInfo('USER.TOAST.CREATED', true); this.toast.showInfo('USER.TOAST.CREATED', true);
this.router.navigate(['users', data.userId]); this.router.navigate(['users', data.userId]);
}) })
.catch(error => { .catch((error) => {
this.loading = false; this.loading = false;
this.toast.showError(error); this.toast.showError(error);
}); });
@ -137,6 +137,9 @@ export class UserCreateComponent implements OnDestroy {
public get email(): AbstractControl | null { public get email(): AbstractControl | null {
return this.userForm.get('email'); return this.userForm.get('email');
} }
public get isVerified(): AbstractControl | null {
return this.userForm.get('isVerified');
}
public get userName(): AbstractControl | null { public get userName(): AbstractControl | null {
return this.userForm.get('userName'); return this.userForm.get('userName');
} }

View File

@ -11,6 +11,7 @@ import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module'; import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
import { InputModule } from 'src/app/modules/input/input.module'; import { InputModule } from 'src/app/modules/input/input.module';
import { UserCreateRoutingModule } from './user-create-routing.module'; import { UserCreateRoutingModule } from './user-create-routing.module';
@ -31,6 +32,7 @@ import { UserCreateComponent } from './user-create.component';
MatCheckboxModule, MatCheckboxModule,
MatTooltipModule, MatTooltipModule,
TranslateModule, TranslateModule,
InfoSectionModule,
DetailLayoutModule, DetailLayoutModule,
InputModule, InputModule,
MatRippleModule, MatRippleModule,

View File

@ -27,11 +27,11 @@
.btn-container { .btn-container {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin: 0 -.5rem; margin: 0 -0.5rem;
button { button {
border-radius: .5rem; border-radius: 0.5rem;
margin: 0 .5rem; margin: 0 0.5rem;
} }
} }
@ -39,11 +39,11 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: stretch; align-items: stretch;
margin: -1.5rem -.5rem; margin: -1.5rem -0.5rem;
.app-card { .app-card {
flex: 1; flex: 1;
margin: .5rem; margin: 0.5rem;
} }
} }
@ -55,6 +55,7 @@
.resendemail { .resendemail {
margin-right: 1rem; margin-right: 1rem;
margin-top: 0.5rem;
} }
.side-padding { .side-padding {

View File

@ -227,9 +227,9 @@ export class AuthUserDetailComponent implements OnDestroy {
width: '400px', width: '400px',
}); });
dialogRefPhone.afterClosed().subscribe((resp) => { dialogRefPhone.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => {
if (resp) { if (resp && resp.value) {
this.savePhone(resp); this.savePhone(resp.value);
} }
}); });
break; break;
@ -247,9 +247,9 @@ export class AuthUserDetailComponent implements OnDestroy {
width: '400px', width: '400px',
}); });
dialogRefEmail.afterClosed().subscribe((resp) => { dialogRefEmail.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => {
if (resp) { if (resp && resp.value) {
this.saveEmail(resp); this.saveEmail(resp.value);
} }
}); });
break; break;

View File

@ -9,6 +9,16 @@
<input [formControl]="valueControl" cnslInput <input [formControl]="valueControl" cnslInput
(keydown.enter)="valueControl.valid ? closeDialogWithValue() : null" /> (keydown.enter)="valueControl.valid ? closeDialogWithValue() : null" />
</cnsl-form-field> </cnsl-form-field>
<ng-container *ngIf="data.type === EditDialogType.EMAIL && data.isVerifiedTextKey">
<mat-checkbox class="verified-checkbox" [(ngModel)]="isVerified">
{{data.isVerifiedTextKey |
translate}}
</mat-checkbox>
<cnsl-info-section class="full-width desc">
<span>{{data.isVerifiedTextDescKey | translate}}</span>
</cnsl-info-section>
</ng-container>
</div> </div>
<div mat-dialog-actions class="action"> <div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-button class="ok-button" (click)="closeDialog()"> <button cdkFocusInitial color="primary" mat-button class="ok-button" (click)="closeDialog()">

View File

@ -2,6 +2,10 @@
width: 100%; width: 100%;
} }
.verified-checkbox {
margin-top: 1.5rem;
}
.desc { .desc {
font-size: 14px; font-size: 14px;
color: var(--grey); color: var(--grey);
@ -12,10 +16,10 @@
justify-content: flex-end; justify-content: flex-end;
.ok-button { .ok-button {
margin-left: .5rem; margin-left: 0.5rem;
} }
button { button {
border-radius: .5rem; border-radius: 0.5rem;
} }
} }

View File

@ -15,16 +15,17 @@ export enum EditDialogType {
}) })
export class EditDialogComponent { export class EditDialogComponent {
public isPhone: boolean = false; public isPhone: boolean = false;
public isVerified: boolean = false;
public phoneCountry: string = 'CH'; public phoneCountry: string = 'CH';
public valueControl: FormControl = new FormControl(['', [Validators.required]]); public valueControl: FormControl = new FormControl(['', [Validators.required]]);
constructor(public dialogRef: MatDialogRef<EditDialogComponent>, public EditDialogType: any = EditDialogType;
@Inject(MAT_DIALOG_DATA) public data: any) { constructor(public dialogRef: MatDialogRef<EditDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {
this.valueControl.setValue(data.value); this.valueControl.setValue(data.value);
if (data.type === EditDialogType.PHONE) { if (data.type === EditDialogType.PHONE) {
this.isPhone = true; this.isPhone = true;
} }
this.valueControl.valueChanges.subscribe(value => { this.valueControl.valueChanges.subscribe((value) => {
if (value && value.length > 1) { if (value && value.length > 1) {
this.changeValue(value); this.changeValue(value);
} }
@ -53,6 +54,6 @@ export class EditDialogComponent {
} }
closeDialogWithValue(): void { closeDialogWithValue(): void {
this.dialogRef.close(this.valueControl.value); this.dialogRef.close({ value: this.valueControl.value, isVerified: this.isVerified });
} }
} }

View File

@ -19,7 +19,7 @@
} }
p { p {
margin: .5rem 0; margin: 0.5rem 0;
font-size: 14px; font-size: 14px;
color: var(--grey); color: var(--grey);
} }
@ -30,13 +30,13 @@
} }
.actions-trigger { .actions-trigger {
margin-top: .25rem; margin-top: 0.25rem;
display: flex; display: flex;
align-items: center; align-items: center;
.icon { .icon {
margin-left: .5rem; margin-left: 0.5rem;
margin-right: -.5rem; margin-right: -0.5rem;
} }
} }
} }
@ -66,3 +66,7 @@
.side-padding { .side-padding {
padding-top: 1rem; padding-top: 1rem;
} }
.resendemail {
margin-top: 0.5rem;
}

View File

@ -229,10 +229,10 @@ export class UserDetailComponent implements OnInit {
}); });
} }
public saveEmail(email: string): void { public saveEmail(email: string, isVerified: boolean): void {
if (this.user.id && email) { if (this.user.id && email) {
this.mgmtUserService this.mgmtUserService
.updateHumanEmail(this.user.id, email) .updateHumanEmail(this.user.id, email, isVerified)
.then(() => { .then(() => {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true); this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
if (this.user.state === UserState.USER_STATE_INITIAL) { if (this.user.state === UserState.USER_STATE_INITIAL) {
@ -355,9 +355,9 @@ export class UserDetailComponent implements OnInit {
width: '400px', width: '400px',
}); });
dialogRefPhone.afterClosed().subscribe((resp) => { dialogRefPhone.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => {
if (resp) { if (resp && resp.value) {
this.savePhone(resp); this.savePhone(resp.value);
} }
}); });
break; break;
@ -369,15 +369,17 @@ export class UserDetailComponent implements OnInit {
labelKey: 'ACTIONS.NEWVALUE', labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE', titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC', descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
isVerifiedTextKey: 'USER.LOGINMETHODS.EMAIL.ISVERIFIED',
isVerifiedTextDescKey: 'USER.LOGINMETHODS.EMAIL.ISVERIFIEDDESC',
value: this.user.human?.email?.email, value: this.user.human?.email?.email,
type: EditDialogType.EMAIL, type: EditDialogType.EMAIL,
}, },
width: '400px', width: '400px',
}); });
dialogRefEmail.afterClosed().subscribe((resp) => { dialogRefEmail.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => {
if (resp) { if (resp && resp.value) {
this.saveEmail(resp); this.saveEmail(resp.value, resp.isVerified);
} }
}); });
break; break;

View File

@ -899,32 +899,24 @@ export class ManagementService {
return this.grpcService.mgmt.listHumanLinkedIDPs(req, null).then((resp) => resp.toObject()); return this.grpcService.mgmt.listHumanLinkedIDPs(req, null).then((resp) => resp.toObject());
} }
public getAction( public getAction(id: string): Promise<GetActionResponse.AsObject> {
id: string,
): Promise<GetActionResponse.AsObject> {
const req = new GetActionRequest(); const req = new GetActionRequest();
req.setId(id); req.setId(id);
return this.grpcService.mgmt.getAction(req, null).then(resp => resp.toObject()); return this.grpcService.mgmt.getAction(req, null).then((resp) => resp.toObject());
} }
public createAction( public createAction(req: CreateActionRequest): Promise<CreateActionResponse.AsObject> {
req: CreateActionRequest, return this.grpcService.mgmt.createAction(req, null).then((resp) => resp.toObject());
): Promise<CreateActionResponse.AsObject> {
return this.grpcService.mgmt.createAction(req, null).then(resp => resp.toObject());
} }
public updateAction( public updateAction(req: UpdateActionRequest): Promise<UpdateActionResponse.AsObject> {
req: UpdateActionRequest, return this.grpcService.mgmt.updateAction(req, null).then((resp) => resp.toObject());
): Promise<UpdateActionResponse.AsObject> {
return this.grpcService.mgmt.updateAction(req, null).then(resp => resp.toObject());
} }
public deleteAction( public deleteAction(id: string): Promise<DeleteActionResponse.AsObject> {
id: string,
): Promise<DeleteActionResponse.AsObject> {
const req = new DeleteActionRequest(); const req = new DeleteActionRequest();
req.setId(id); req.setId(id);
return this.grpcService.mgmt.deleteAction(req, null).then(resp => resp.toObject()); return this.grpcService.mgmt.deleteAction(req, null).then((resp) => resp.toObject());
} }
public listActions( public listActions(
@ -949,23 +941,19 @@ export class ManagementService {
metadata.setAsc(asc); metadata.setAsc(asc);
} }
req.setQuery(metadata); req.setQuery(metadata);
return this.grpcService.mgmt.listActions(req, null).then(resp => resp.toObject()); return this.grpcService.mgmt.listActions(req, null).then((resp) => resp.toObject());
} }
public getFlow( public getFlow(type: FlowType): Promise<GetFlowResponse.AsObject> {
type: FlowType
): Promise<GetFlowResponse.AsObject> {
const req = new GetFlowRequest(); const req = new GetFlowRequest();
req.setType(type); req.setType(type);
return this.grpcService.mgmt.getFlow(req, null).then(resp => resp.toObject()); return this.grpcService.mgmt.getFlow(req, null).then((resp) => resp.toObject());
} }
public clearFlow( public clearFlow(type: FlowType): Promise<ClearFlowResponse.AsObject> {
type: FlowType
): Promise<ClearFlowResponse.AsObject> {
const req = new ClearFlowRequest(); const req = new ClearFlowRequest();
req.setType(type); req.setType(type);
return this.grpcService.mgmt.clearFlow(req, null).then(resp => resp.toObject()); return this.grpcService.mgmt.clearFlow(req, null).then((resp) => resp.toObject());
} }
public setTriggerActions( public setTriggerActions(
@ -977,7 +965,7 @@ export class ManagementService {
req.setActionIdsList(actionIdsList); req.setActionIdsList(actionIdsList);
req.setFlowType(type); req.setFlowType(type);
req.setTriggerType(triggerType); req.setTriggerType(triggerType);
return this.grpcService.mgmt.setTriggerActions(req, null).then(resp => resp.toObject()); return this.grpcService.mgmt.setTriggerActions(req, null).then((resp) => resp.toObject());
} }
public getIAM(): Promise<GetIAMResponse.AsObject> { public getIAM(): Promise<GetIAMResponse.AsObject> {
@ -1468,10 +1456,13 @@ export class ManagementService {
return this.grpcService.mgmt.getHumanEmail(req, null).then((resp) => resp.toObject()); return this.grpcService.mgmt.getHumanEmail(req, null).then((resp) => resp.toObject());
} }
public updateHumanEmail(userId: string, email: string): Promise<UpdateHumanEmailResponse.AsObject> { public updateHumanEmail(userId: string, email: string, isVerified?: boolean): Promise<UpdateHumanEmailResponse.AsObject> {
const req = new UpdateHumanEmailRequest(); const req = new UpdateHumanEmailRequest();
req.setUserId(userId); req.setUserId(userId);
req.setEmail(email); req.setEmail(email);
if (isVerified) {
req.setIsEmailVerified(isVerified);
}
return this.grpcService.mgmt.updateHumanEmail(req, null).then((resp) => resp.toObject()); return this.grpcService.mgmt.updateHumanEmail(req, null).then((resp) => resp.toObject());
} }

View File

@ -419,6 +419,8 @@
"EMAIL": { "EMAIL": {
"TITLE": "E-Mail", "TITLE": "E-Mail",
"VALID": "Validiert", "VALID": "Validiert",
"ISVERIFIED": "Email Verifiziert",
"ISVERIFIEDDESC": "Wenn die Email als verifiziert angegeben wird, wird keine Initialisierungsmail versendet.",
"RESEND": "Verifikationsmail erneut senden", "RESEND": "Verifikationsmail erneut senden",
"EDITTITLE": "Email ändern", "EDITTITLE": "Email ändern",
"EDITDESC": "Geben Sie die neue Email in dem darunterliegenden Feld ein!" "EDITDESC": "Geben Sie die neue Email in dem darunterliegenden Feld ein!"

View File

@ -419,6 +419,8 @@
"EMAIL": { "EMAIL": {
"TITLE": "E-mail", "TITLE": "E-mail",
"VALID": "validated", "VALID": "validated",
"ISVERIFIED": "Email Verified",
"ISVERIFIEDDESC": "If the email is indicated as verified, no initialization email will be sent.",
"RESEND": "Resend Verification E-mail", "RESEND": "Resend Verification E-mail",
"EDITTITLE": "Change Email", "EDITTITLE": "Change Email",
"EDITDESC": "Enter the new email in the field below." "EDITDESC": "Enter the new email in the field below."

View File

@ -419,6 +419,8 @@
"EMAIL": { "EMAIL": {
"TITLE": "E-mail", "TITLE": "E-mail",
"VALID": "convalidato", "VALID": "convalidato",
"ISVERIFIED": "Email Verificato",
"ISVERIFIEDDESC": "Se l'email viene indicata come verificata, non verrà inviata alcuna email di inizializzazione.",
"RESEND": "Invia di nuovo l'e-mail di verifica", "RESEND": "Invia di nuovo l'e-mail di verifica",
"EDITTITLE": "Cambiare l'e-mail", "EDITTITLE": "Cambiare l'e-mail",
"EDITDESC": "Inserisci la nuova email nel campo sottostante." "EDITDESC": "Inserisci la nuova email nel campo sottostante."